import archiver from 'archiver'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import type { SessionTrace } from './session-trace.js'; import type { ScenarioModel } from '../model/scenario-model.js'; import type { BugReportOptions, BugReportResult } from './types.js'; import { validateScenario } from '../validation/validator.js'; import { getValkyrieLogPath, readLogTail } from './valkyrie-log-finder.js'; const README_TEXT = `Valkyrie MoM MCP - Bug Report ============================== This ZIP was generated by the export_bug_report tool. Contents: report.json - Session metadata and trace of MCP tool calls validation.json - Validation results at time of export scenario/ - Scenario INI files (if a scenario was loaded) valkyrie-logs/ - Valkyrie Player.log tail (if found) How to file an issue: 1. Go to https://github.com/thijs-hakkenberg/ValkyrieMCP/issues/new 2. Paste the issue template from the tool output 3. Drag and drop this ZIP file into the issue `; export async function buildBugReport( trace: SessionTrace, model: ScenarioModel | null, options: BugReportOptions = {}, ): Promise { const { outputDir = os.tmpdir(), includeScenario = true, includeValkyrieLog = true, } = options; const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const zipName = `valkyrie-bug-report-${timestamp}.zip`; const zipPath = path.join(outputDir, zipName); // Build ZIP const output = fs.createWriteStream(zipPath); const archive = archiver('zip'); const done = new Promise((resolve, reject) => { output.on('close', resolve); archive.on('error', reject); }); archive.pipe(output); // report.json const metadata = trace.getMetadata(); const entries = trace.getEntries(); const report = { metadata, traceEntries: entries, exportedAt: new Date().toISOString(), }; archive.append(JSON.stringify(report, null, 2), { name: 'report.json' }); // validation.json let validationData: any = { errors: [], warnings: [], note: 'No scenario loaded' }; if (model) { const results = validateScenario(model); const errors = results.filter(r => r.severity === 'error'); const warnings = results.filter(r => r.severity === 'warning'); validationData = { errors, warnings }; } archive.append(JSON.stringify(validationData, null, 2), { name: 'validation.json' }); // README.txt archive.append(README_TEXT, { name: 'README.txt' }); // scenario/ directory if (includeScenario && model?.scenarioDir && fs.existsSync(model.scenarioDir)) { const scenarioFiles = fs.readdirSync(model.scenarioDir); for (const file of scenarioFiles) { const filePath = path.join(model.scenarioDir, file); if (fs.statSync(filePath).isFile()) { archive.file(filePath, { name: `scenario/${file}` }); } } } // valkyrie-logs/ if (includeValkyrieLog) { const logPath = getValkyrieLogPath(); if (logPath) { const logContent = await readLogTail(logPath); if (logContent) { archive.append(logContent, { name: 'valkyrie-logs/Player.log' }); } } } await archive.finalize(); await done; // Build issue template const errorEntries = trace.getErrorEntries(); const allEntries = trace.getEntries(); const errorCount = errorEntries.length; const validationErrors = model ? validateScenario(model).filter(r => r.severity === 'error').length : 0; const validationWarnings = model ? validateScenario(model).filter(r => r.severity === 'warning').length : 0; const recentErrors = errorEntries.slice(-5).map(e => `- \`${e.tool}\` (${e.timestamp}): ${e.resultSummary.replace(/^Error:\s*/, '').slice(0, 100)}` ).join('\n'); const issueTitle = `[Bug Report] MCP session with ${allEntries.length} tool calls, ${errorCount} errors`; const issueBody = `## Bug Report **Environment:** - MCP Server: v${metadata.serverVersion} - Node: ${metadata.nodeVersion} - Platform: ${metadata.platform} ${metadata.arch} **Session Summary:** - Tool calls: ${allEntries.length} - Errors: ${errorCount} - Validation: ${validationErrors} errors, ${validationWarnings} warnings ## What happened? ## Steps to reproduce ${recentErrors ? `## Recent errors from session trace\n${recentErrors}\n` : ''} ## Attachments `; const escapedTitle = issueTitle.replace(/"/g, '\\"'); const ghCommand = `gh issue create --repo thijs-hakkenberg/ValkyrieMCP --title "${escapedTitle}" --body-file -`; return { zipPath, issueTitle, issueBody, ghCommand }; }