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
JavaScript
;
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;