UNPKG

onerios-mcp-server

Version:

OneriosMCP server providing memory, backlog management, file operations, and utility functions for enhanced AI assistant capabilities

475 lines (474 loc) 21.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WikiPageSchema = exports.WikiGenerationOptionsSchema = exports.GitHubWikiGenerator = void 0; const zod_1 = require("zod"); const github_integration_js_1 = require("./github-integration.js"); const github_token_manager_js_1 = require("./github-token-manager.js"); // Schema definitions for wiki generation parameters const WikiGenerationOptionsSchema = zod_1.z.object({ repository: zod_1.z.string().describe('Repository in format owner/repo'), includeProgress: zod_1.z.boolean().default(true).describe('Include progress statistics'), includeTaskBreakdown: zod_1.z.boolean().default(true).describe('Include detailed task breakdown'), includeTimeline: zod_1.z.boolean().default(false).describe('Include project timeline'), templateType: zod_1.z.enum(['standard', 'detailed', 'compact']).default('standard'), sections: zod_1.z.array(zod_1.z.enum([ 'overview', 'progress', 'issues', 'tasks', 'timeline', 'statistics', 'team' ])).default(['overview', 'progress', 'issues', 'tasks']) }); exports.WikiGenerationOptionsSchema = WikiGenerationOptionsSchema; const WikiPageSchema = zod_1.z.object({ title: zod_1.z.string(), content: zod_1.z.string(), path: zod_1.z.string(), lastUpdated: zod_1.z.string() }); exports.WikiPageSchema = WikiPageSchema; /** * GitHub Wiki Generator * * Generates automated wiki pages from backlog data with comprehensive * project documentation, progress tracking, and customizable templates. */ class GitHubWikiGenerator { constructor() { this.apiClient = null; // API client will be initialized when needed with token } /** * Generate wiki pages from backlog data */ async generateWikiPages(backlogData, options) { const validatedOptions = WikiGenerationOptionsSchema.parse(options); const pages = []; // Generate main project overview page if (validatedOptions.sections.includes('overview')) { pages.push(await this.generateProjectOverview(backlogData, validatedOptions)); } // Generate progress tracking page if (validatedOptions.sections.includes('progress')) { pages.push(await this.generateProgressPage(backlogData, validatedOptions)); } // Generate detailed issues page if (validatedOptions.sections.includes('issues')) { pages.push(await this.generateIssuesPage(backlogData, validatedOptions)); } // Generate tasks breakdown page if (validatedOptions.sections.includes('tasks')) { pages.push(await this.generateTasksPage(backlogData, validatedOptions)); } // Generate timeline page (if enabled) if (validatedOptions.sections.includes('timeline') && validatedOptions.includeTimeline) { pages.push(await this.generateTimelinePage(backlogData, validatedOptions)); } // Generate statistics page if (validatedOptions.sections.includes('statistics')) { pages.push(await this.generateStatisticsPage(backlogData, validatedOptions)); } return pages; } /** * Publish wiki pages to GitHub repository */ async publishToGitHub(pages, repository, token) { try { // Use provided token or retrieve from secure storage let authToken = token; if (!authToken) { const storedTokens = await github_token_manager_js_1.GitHubTokenSecurityManager.listTokens(); if (storedTokens.length === 0) { throw new Error('No GitHub tokens available. Please authenticate first.'); } // Use the most recently used token const tokenData = await github_token_manager_js_1.GitHubTokenSecurityManager.retrieveToken(storedTokens[0].id); authToken = tokenData.token; } // Initialize and authenticate GitHub client this.apiClient = new github_integration_js_1.GitHubAPIClient({ token: authToken }); await this.apiClient.authenticate(); const publishedPages = []; const errors = []; // Check if repository has wiki enabled const repoInfo = await this.getRepositoryInfo(repository, authToken); if (!repoInfo.has_wiki) { throw new Error(`Wiki is not enabled for repository ${repository}`); } // Publish each page for (const page of pages) { try { await this.publishWikiPage(repository, page, authToken); publishedPages.push(page.title); } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Unknown error'; errors.push(`Failed to publish "${page.title}": ${errorMsg}`); } } return { success: errors.length === 0, publishedPages, errors }; } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Unknown error'; return { success: false, publishedPages: [], errors: [errorMsg] }; } } /** * Generate project overview page */ async generateProjectOverview(backlogData, options) { const { statistics } = backlogData; const timestamp = new Date().toISOString(); let content = `# Project Overview\n\n`; content += `*Last updated: ${new Date().toLocaleString()}*\n\n`; // Project status summary content += `## 📊 Project Status\n\n`; content += `- **Total Issues**: ${statistics.totalIssues}\n`; content += `- **Total Tasks**: ${statistics.totalTasks}\n`; content += `- **Active Issue**: ${statistics.activeIssue || 'None'}\n\n`; // Progress overview if (options.includeProgress) { const completedIssues = statistics.issuesByStatus.Done || 0; const completedTasks = statistics.tasksByStatus.Done || 0; const issueProgress = statistics.totalIssues > 0 ? Math.round((completedIssues / statistics.totalIssues) * 100) : 0; const taskProgress = statistics.totalTasks > 0 ? Math.round((completedTasks / statistics.totalTasks) * 100) : 0; content += `## 🎯 Progress Summary\n\n`; content += `### Issues Progress\n`; content += `\`\`\`\n`; content += `${this.generateProgressBar(issueProgress)} ${issueProgress}% (${completedIssues}/${statistics.totalIssues})\n`; content += `\`\`\`\n\n`; content += `### Tasks Progress\n`; content += `\`\`\`\n`; content += `${this.generateProgressBar(taskProgress)} ${taskProgress}% (${completedTasks}/${statistics.totalTasks})\n`; content += `\`\`\`\n\n`; } // Issues overview content += `## 📋 Issues Overview\n\n`; content += `| Status | Count |\n`; content += `|--------|-------|\n`; Object.entries(statistics.issuesByStatus).forEach(([status, count]) => { const emoji = this.getStatusEmoji(status); content += `| ${emoji} ${status} | ${count} |\n`; }); content += `\n`; // Quick navigation content += `## 🔗 Quick Navigation\n\n`; content += `- [📈 Progress Details](Progress-Report)\n`; content += `- [📋 Issues Breakdown](Issues-Breakdown)\n`; content += `- [✅ Tasks Overview](Tasks-Overview)\n`; content += `- [📊 Statistics](Project-Statistics)\n`; if (options.includeTimeline) { content += `- [📅 Timeline](Project-Timeline)\n`; } return { title: 'Project Overview', content, path: 'Project-Overview.md', lastUpdated: timestamp }; } /** * Generate progress tracking page */ async generateProgressPage(backlogData, options) { const { issues, statistics } = backlogData; const timestamp = new Date().toISOString(); let content = `# Progress Report\n\n`; content += `*Last updated: ${new Date().toLocaleString()}*\n\n`; // Overall progress metrics const completedIssues = statistics.issuesByStatus.Done || 0; const completedTasks = statistics.tasksByStatus.Done || 0; const inWorkIssues = statistics.issuesByStatus.InWork || 0; const inWorkTasks = statistics.tasksByStatus.InWork || 0; content += `## 📊 Overall Progress\n\n`; content += `### Issues\n`; content += `- ✅ Completed: ${completedIssues}/${statistics.totalIssues}\n`; content += `- 🔄 In Progress: ${inWorkIssues}\n`; content += `- 📝 Remaining: ${statistics.issuesByStatus.New || 0}\n\n`; content += `### Tasks\n`; content += `- ✅ Completed: ${completedTasks}/${statistics.totalTasks}\n`; content += `- 🔄 In Progress: ${inWorkTasks}\n`; content += `- 📝 Remaining: ${statistics.tasksByStatus.New || 0}\n\n`; // Progress by issue content += `## 📋 Progress by Issue\n\n`; issues.forEach(issue => { const completedTasks = issue.tasks.filter(t => t.status === 'Done').length; const totalTasks = issue.tasks.length; const progress = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0; const statusEmoji = this.getStatusEmoji(issue.status); content += `### ${statusEmoji} ${issue.name}\n`; content += `**Status**: ${issue.status} | **Progress**: ${progress}% (${completedTasks}/${totalTasks} tasks)\n\n`; if (options.templateType === 'detailed' && issue.description) { content += `**Description**: ${issue.description}\n\n`; } content += `\`\`\`\n`; content += `${this.generateProgressBar(progress)} ${progress}%\n`; content += `\`\`\`\n\n`; // Task breakdown for in-progress issues if (issue.status === 'InWork' && options.includeTaskBreakdown) { content += `**Task Breakdown**:\n`; issue.tasks.forEach(task => { const taskEmoji = this.getStatusEmoji(task.status); content += `- ${taskEmoji} ${task.title}\n`; }); content += `\n`; } }); return { title: 'Progress Report', content, path: 'Progress-Report.md', lastUpdated: timestamp }; } /** * Generate detailed issues page */ async generateIssuesPage(backlogData, options) { const { issues } = backlogData; const timestamp = new Date().toISOString(); let content = `# Issues Breakdown\n\n`; content += `*Last updated: ${new Date().toLocaleString()}*\n\n`; // Group issues by status const issuesByStatus = issues.reduce((acc, issue) => { if (!acc[issue.status]) acc[issue.status] = []; acc[issue.status].push(issue); return acc; }, {}); // Generate sections for each status Object.entries(issuesByStatus).forEach(([status, statusIssues]) => { const statusEmoji = this.getStatusEmoji(status); content += `## ${statusEmoji} ${status} Issues (${statusIssues.length})\n\n`; statusIssues.forEach(issue => { content += `### ${issue.name}\n`; if (issue.description) { content += `${issue.description}\n\n`; } const completedTasks = issue.tasks.filter(t => t.status === 'Done').length; const totalTasks = issue.tasks.length; const progress = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0; content += `**Progress**: ${progress}% (${completedTasks}/${totalTasks} tasks completed)\n\n`; if (options.includeTaskBreakdown) { content += `**Tasks**:\n`; issue.tasks.forEach(task => { const taskEmoji = this.getStatusEmoji(task.status); content += `- ${taskEmoji} **${task.title}**\n`; if (task.description && options.templateType === 'detailed') { content += ` ${task.description}\n`; } }); content += `\n`; } content += `---\n\n`; }); }); return { title: 'Issues Breakdown', content, path: 'Issues-Breakdown.md', lastUpdated: timestamp }; } /** * Generate tasks overview page */ async generateTasksPage(backlogData, options) { const { issues } = backlogData; const timestamp = new Date().toISOString(); let content = `# Tasks Overview\n\n`; content += `*Last updated: ${new Date().toLocaleString()}*\n\n`; // Collect all tasks const allTasks = issues.flatMap(issue => issue.tasks.map(task => ({ ...task, issueName: issue.name, issueStatus: issue.status }))); // Group tasks by status const tasksByStatus = allTasks.reduce((acc, task) => { if (!acc[task.status]) acc[task.status] = []; acc[task.status].push(task); return acc; }, {}); // Generate sections for each status Object.entries(tasksByStatus).forEach(([status, statusTasks]) => { const statusEmoji = this.getStatusEmoji(status); content += `## ${statusEmoji} ${status} Tasks (${statusTasks.length})\n\n`; statusTasks.forEach(task => { content += `### ${task.title}\n`; content += `**Issue**: ${task.issueName}\n`; if (task.description) { content += `**Description**: ${task.description}\n`; } content += `\n`; }); content += `---\n\n`; }); return { title: 'Tasks Overview', content, path: 'Tasks-Overview.md', lastUpdated: timestamp }; } /** * Generate timeline page */ async generateTimelinePage(backlogData, options) { const timestamp = new Date().toISOString(); let content = `# Project Timeline\n\n`; content += `*Last updated: ${new Date().toLocaleString()}*\n\n`; content += `> ⚠️ Timeline tracking is not yet implemented.\n`; content += `> This feature requires task completion dates and dependency tracking.\n\n`; content += `## Planned Features\n\n`; content += `- [ ] Task completion timestamps\n`; content += `- [ ] Dependency visualization\n`; content += `- [ ] Milestone tracking\n`; content += `- [ ] Gantt chart integration\n`; return { title: 'Project Timeline', content, path: 'Project-Timeline.md', lastUpdated: timestamp }; } /** * Generate statistics page */ async generateStatisticsPage(backlogData, options) { const { issues, statistics } = backlogData; const timestamp = new Date().toISOString(); let content = `# Project Statistics\n\n`; content += `*Last updated: ${new Date().toLocaleString()}*\n\n`; // Overview metrics content += `## 📊 Overview Metrics\n\n`; content += `| Metric | Value |\n`; content += `|--------|-------|\n`; content += `| Total Issues | ${statistics.totalIssues} |\n`; content += `| Total Tasks | ${statistics.totalTasks} |\n`; content += `| Average Tasks per Issue | ${statistics.totalIssues > 0 ? Math.round(statistics.totalTasks / statistics.totalIssues * 10) / 10 : 0} |\n`; content += `| Completion Rate (Issues) | ${statistics.totalIssues > 0 ? Math.round((statistics.issuesByStatus.Done || 0) / statistics.totalIssues * 100) : 0}% |\n`; content += `| Completion Rate (Tasks) | ${statistics.totalTasks > 0 ? Math.round((statistics.tasksByStatus.Done || 0) / statistics.totalTasks * 100) : 0}% |\n`; content += `\n`; // Status distribution charts (ASCII) content += `## 📈 Status Distribution\n\n`; content += `### Issues by Status\n`; content += `\`\`\`\n`; Object.entries(statistics.issuesByStatus).forEach(([status, count]) => { const percentage = statistics.totalIssues > 0 ? Math.round((count / statistics.totalIssues) * 100) : 0; const bar = '█'.repeat(Math.floor(percentage / 5)); content += `${status.padEnd(8)}${bar.padEnd(20)} ${count} (${percentage}%)\n`; }); content += `\`\`\`\n\n`; content += `### Tasks by Status\n`; content += `\`\`\`\n`; Object.entries(statistics.tasksByStatus).forEach(([status, count]) => { const percentage = statistics.totalTasks > 0 ? Math.round((count / statistics.totalTasks) * 100) : 0; const bar = '█'.repeat(Math.floor(percentage / 5)); content += `${status.padEnd(8)}${bar.padEnd(20)} ${count} (${percentage}%)\n`; }); content += `\`\`\`\n\n`; // Issue complexity analysis content += `## 🧮 Issue Complexity Analysis\n\n`; const issueComplexity = issues.map(issue => ({ name: issue.name, taskCount: issue.tasks.length, complexity: issue.tasks.length <= 3 ? 'Simple' : issue.tasks.length <= 7 ? 'Medium' : 'Complex' })); const complexityGroups = issueComplexity.reduce((acc, issue) => { if (!acc[issue.complexity]) acc[issue.complexity] = 0; acc[issue.complexity]++; return acc; }, {}); content += `| Complexity | Count | Issues |\n`; content += `|------------|-------|--------|\n`; Object.entries(complexityGroups).forEach(([complexity, count]) => { const issueNames = issueComplexity .filter(i => i.complexity === complexity) .map(i => i.name) .join(', '); content += `| ${complexity} | ${count} | ${issueNames} |\n`; }); return { title: 'Project Statistics', content, path: 'Project-Statistics.md', lastUpdated: timestamp }; } /** * Get repository information */ async getRepositoryInfo(repository, token) { const [owner, repo] = repository.split('/'); const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'MCP-Wiki-Generator' } }); if (!response.ok) { throw new Error(`Failed to get repository info: ${response.statusText}`); } return response.json(); } /** * Publish a single wiki page to GitHub */ async publishWikiPage(repository, page, token) { // GitHub Wiki API endpoints const [owner, repo] = repository.split('/'); const wikiUrl = `https://api.github.com/repos/${owner}/${repo}/wiki/${page.path}`; const response = await fetch(wikiUrl, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json', 'User-Agent': 'MCP-Wiki-Generator' }, body: JSON.stringify({ message: `Update ${page.title} wiki page`, content: page.content, sha: undefined // GitHub will handle this for us }) }); if (!response.ok) { throw new Error(`Failed to publish wiki page "${page.title}": ${response.statusText}`); } } /** * Get status emoji for visual representation */ getStatusEmoji(status) { const emojiMap = { 'New': '📝', 'InWork': '🔄', 'Done': '✅' }; return emojiMap[status] || '❓'; } /** * Generate ASCII progress bar */ generateProgressBar(percentage, length = 20) { const filled = Math.floor((percentage / 100) * length); const empty = length - filled; return '█'.repeat(filled) + '░'.repeat(empty); } } exports.GitHubWikiGenerator = GitHubWikiGenerator;