import fs from 'fs-extra';
import path from 'path';
import { format } from 'date-fns';
import { SessionSummary, SavedSummaryFile } from './types.js';
export class FileManager {
private readonly sunDir = '.sun';
constructor() {
// Don't create directory in constructor to avoid blocking
}
/**
* Ensure .sun directory exists
*/
private async ensureSunDirectory(): Promise<void> {
try {
// Get current working directory
const cwd = process.cwd();
console.error(`📁 Current working directory: ${cwd}`);
// Try to create .sun directory in current directory
const targetDir = path.resolve(cwd, this.sunDir);
await fs.ensureDir(targetDir);
console.error(`✅ .sun directory created at: ${targetDir}`);
} catch (error) {
console.error('❌ Failed to create .sun directory in current directory:', error);
// Try to create in user's home directory as fallback
const homeDir = process.env.HOME || process.env.USERPROFILE;
if (homeDir) {
try {
const fallbackSunDir = path.join(homeDir, '.sun');
await fs.ensureDir(fallbackSunDir);
console.error(`✅ Created .sun directory in home directory: ${fallbackSunDir}`);
// Update sunDir to use fallback
(this as any).sunDir = fallbackSunDir;
return;
} catch (fallbackError) {
console.error('❌ Failed to create .sun directory in home directory:', fallbackError);
}
}
// Final fallback to temp directory
try {
const tempSunDir = path.join('/tmp', '.sun');
await fs.ensureDir(tempSunDir);
console.error(`✅ Created .sun directory in temp location: ${tempSunDir}`);
(this as any).sunDir = tempSunDir;
} catch (tempError) {
console.error('❌ Failed to create .sun directory in temp location:', tempError);
throw new Error(`Cannot create .sun directory anywhere. Please check permissions.`);
}
}
}
/**
* Generate filename based on current date/time and functionality
*/
private generateFilename(functionality: string): string {
const timestamp = format(new Date(), 'yyyyMMdd_HHmmss');
const sanitizedFunc = functionality
.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_') // Allow Chinese characters
.replace(/_+/g, '_')
.replace(/^_|_$/g, '');
return `${timestamp}_${sanitizedFunc}.mdc`;
}
/**
* Save session summary to .mdc file
*/
async saveSummary(summary: SessionSummary, functionality?: string): Promise<SavedSummaryFile> {
await this.ensureSunDirectory();
const func = functionality || summary.title || 'session_summary';
const filename = this.generateFilename(func);
const filePath = path.join(this.sunDir, filename);
// Create markdown content
const content = this.formatSummaryAsMarkdown(summary);
try {
await fs.writeFile(filePath, content, 'utf-8');
const savedFile: SavedSummaryFile = {
filename,
path: filePath,
summary,
createdAt: new Date().toISOString()
};
console.log(`Summary saved to: ${filePath}`);
return savedFile;
} catch (error) {
console.error('Failed to save summary:', error);
throw error;
}
}
/**
* Format summary as markdown content
*/
private formatSummaryAsMarkdown(summary: SessionSummary): string {
const isEnglish = summary.language === 'en';
const content = `# ${summary.title}
## ${isEnglish ? 'Session Overview' : '会话概要'}
**${isEnglish ? 'Timestamp' : '时间戳'}**: ${summary.timestamp}
**${isEnglish ? 'Completion Status' : '完成状态'}**: ${summary.completionStatus}
**${isEnglish ? 'Message Count' : '消息数量'}**: ${summary.messageCount}
**${isEnglish ? 'Main Functionality' : '主要功能'}**: ${summary.functionality}
## ${isEnglish ? 'Core Essence' : '核心精髓'}
${summary.essence}
## ${isEnglish ? 'Key Points' : '关键要点'}
${summary.keyPoints.map(point => `- ${point}`).join('\n')}
## ${isEnglish ? 'Outcomes' : '完成成果'}
${summary.outcomes.map(outcome => `- ${outcome}`).join('\n')}
${summary.nextSteps && summary.nextSteps.length > 0 ? `## ${isEnglish ? 'Next Steps' : '后续步骤'}
${summary.nextSteps.map(step => `- ${step}`).join('\n')}` : ''}
---
*Generated by Sun Session Summarizer MCP Server*
*Created at: ${new Date().toISOString()}*
`;
return content;
}
/**
* List all saved summary files
*/
async listSummaries(): Promise<SavedSummaryFile[]> {
await this.ensureSunDirectory();
try {
const files = await fs.readdir(this.sunDir);
const mdcFiles = files.filter(file => file.endsWith('.mdc'));
const summaries: SavedSummaryFile[] = [];
for (const filename of mdcFiles) {
const filePath = path.join(this.sunDir, filename);
const stats = await fs.stat(filePath);
// Try to parse the summary from the file
try {
const content = await fs.readFile(filePath, 'utf-8');
const summary = this.parseSummaryFromMarkdown(content, filename);
summaries.push({
filename,
path: filePath,
summary,
createdAt: stats.birthtime.toISOString()
});
} catch (parseError) {
console.warn(`Failed to parse summary from ${filename}:`, parseError);
}
}
// Sort by creation time (newest first)
return summaries.sort((a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
} catch (error) {
console.error('Failed to list summaries:', error);
throw error;
}
}
/**
* Parse summary from markdown content (basic implementation)
*/
private parseSummaryFromMarkdown(content: string, filename: string): SessionSummary {
// Detect language from content
const isEnglish = content.includes('Session Overview') || content.includes('Timestamp');
// Extract title (first # heading)
const titleMatch = content.match(/^# (.+)$/m);
const title = titleMatch ? titleMatch[1] : filename.replace('.mdc', '');
// Extract basic info (support both languages)
const timestampMatch = content.match(/\*\*(Timestamp|时间戳)\*\*: (.+)$/m);
const statusMatch = content.match(/\*\*(Completion Status|完成状态)\*\*: (.+)$/m);
const messageCountMatch = content.match(/\*\*(Message Count|消息数量)\*\*: (\d+)$/m);
// Extract essence (support both languages)
const essenceMatch = content.match(/## (Core Essence|核心精髓)\n([\s\S]*?)\n\n## /) ||
content.match(/## (Core Essence|核心精髓)\n([\s\S]*?)$/);
const essence = essenceMatch ? essenceMatch[2].trim() : '';
// Extract key points (support both languages)
const keyPointsMatch = content.match(/## (Key Points|关键要点)\n([\s\S]*?)\n\n## /) ||
content.match(/## (Key Points|关键要点)\n([\s\S]*?)$/);
const keyPoints = keyPointsMatch
? keyPointsMatch[2].split('\n').filter(line => line.startsWith('- ')).map(line => line.substring(2))
: [];
// Extract outcomes (support both languages)
const outcomesMatch = content.match(/## (Outcomes|完成成果)\n([\s\S]*?)(\n\n## |\n\n---)/);
const outcomes = outcomesMatch
? outcomesMatch[2].split('\n').filter(line => line.startsWith('- ')).map(line => line.substring(2))
: [];
return {
title,
essence,
completionStatus: (statusMatch ? statusMatch[2] : 'unknown') as any,
keyPoints,
outcomes,
timestamp: timestampMatch ? timestampMatch[2] : new Date().toISOString(),
messageCount: messageCountMatch ? parseInt(messageCountMatch[2]) : 0,
functionality: filename.split('_').slice(2).join('_').replace('.mdc', ''),
language: isEnglish ? 'en' : 'zh'
};
}
/**
* Get specific summary by filename
*/
async getSummary(filename: string): Promise<SavedSummaryFile | null> {
const filePath = path.join(this.sunDir, filename);
try {
const exists = await fs.pathExists(filePath);
if (!exists) {
return null;
}
const content = await fs.readFile(filePath, 'utf-8');
const stats = await fs.stat(filePath);
const summary = this.parseSummaryFromMarkdown(content, filename);
return {
filename,
path: filePath,
summary,
createdAt: stats.birthtime.toISOString()
};
} catch (error) {
console.error(`Failed to get summary ${filename}:`, error);
return null;
}
}
/**
* Delete a summary file
*/
async deleteSummary(filename: string): Promise<boolean> {
const filePath = path.join(this.sunDir, filename);
try {
const exists = await fs.pathExists(filePath);
if (!exists) {
return false;
}
await fs.remove(filePath);
console.log(`Summary deleted: ${filePath}`);
return true;
} catch (error) {
console.error(`Failed to delete summary ${filename}:`, error);
return false;
}
}
}