Skip to main content
Glama

mcp-adr-analysis-server

by tosin2013
interactive-approval.ts16.8 kB
/** * Interactive approval workflow for smart git push * * Handles user interaction for approving/rejecting files with issues */ import { createInterface } from 'readline'; import { writeFileSync } from 'fs'; export interface ApprovalItem { filePath: string; issues: ApprovalIssue[]; suggestions: string[]; severity: 'error' | 'warning' | 'info'; allowedInLocation: boolean; confidence: number; } export interface ApprovalIssue { type: | 'sensitive-content' | 'llm-artifact' | 'location-violation' | 'temporary-file' | 'wrong-location'; message: string; severity: 'error' | 'warning' | 'info'; pattern?: string; line?: number; context?: string; } export interface ApprovalResult { approved: string[]; rejected: string[]; moved: Array<{ from: string; to: string }>; ignored: string[]; actions: ApprovalAction[]; proceed: boolean; } export interface ApprovalAction { type: 'approve' | 'reject' | 'move' | 'ignore' | 'edit'; filePath: string; target?: string | undefined; reason?: string; } export interface ApprovalOptions { interactiveMode: boolean; autoApproveInfo: boolean; autoRejectErrors: boolean; dryRun: boolean; batchMode: boolean; timeout?: number; // seconds } /** * Main interactive approval function */ export async function handleInteractiveApproval( items: ApprovalItem[], options: ApprovalOptions ): Promise<ApprovalResult> { const result: ApprovalResult = { approved: [], rejected: [], moved: [], ignored: [], actions: [], proceed: false, }; if (items.length === 0) { result.proceed = true; return result; } // Handle non-interactive mode if (!options.interactiveMode) { return handleNonInteractiveApproval(items, options); } // Display summary displayApprovalSummary(items); // Group items by severity for better UX const errorItems = items.filter(item => item.severity === 'error'); const warningItems = items.filter(item => item.severity === 'warning'); const infoItems = items.filter(item => item.severity === 'info'); // Handle errors first (these are most critical) if (errorItems.length > 0) { console.log('\n🚨 CRITICAL ISSUES (Must be resolved before proceeding)'); for (const item of errorItems) { const action = await handleItemApproval(item, options, true); result.actions.push(action); applyAction(action, result); } } // Handle warnings if (warningItems.length > 0) { console.log('\n⚠️ WARNING ISSUES (Review recommended)'); for (const item of warningItems) { const action = await handleItemApproval(item, options, false); result.actions.push(action); applyAction(action, result); } } // Handle info items (auto-approve if option set) if (infoItems.length > 0) { if (options.autoApproveInfo) { console.log(`\n✅ Auto-approving ${infoItems.length} info-level items`); for (const item of infoItems) { const action: ApprovalAction = { type: 'approve', filePath: item.filePath }; result.actions.push(action); applyAction(action, result); } } else { console.log('\n💡 INFO ITEMS (Low priority)'); for (const item of infoItems) { const action = await handleItemApproval(item, options, false); result.actions.push(action); applyAction(action, result); } } } // Final confirmation result.proceed = await getFinalConfirmation(result, options); return result; } /** * Handle non-interactive approval */ function handleNonInteractiveApproval( items: ApprovalItem[], options: ApprovalOptions ): ApprovalResult { const result: ApprovalResult = { approved: [], rejected: [], moved: [], ignored: [], actions: [], proceed: false, }; for (const item of items) { let action: ApprovalAction; if (item.severity === 'error' && options.autoRejectErrors) { action = { type: 'reject', filePath: item.filePath, reason: 'Auto-rejected due to errors' }; } else if (item.severity === 'info' && options.autoApproveInfo) { action = { type: 'approve', filePath: item.filePath }; } else { // Default behavior based on severity switch (item.severity) { case 'error': action = { type: 'reject', filePath: item.filePath, reason: 'Automatic rejection due to errors', }; break; case 'warning': action = item.allowedInLocation ? { type: 'approve', filePath: item.filePath } : { type: 'reject', filePath: item.filePath, reason: 'Location violation' }; break; case 'info': action = { type: 'approve', filePath: item.filePath }; break; } } result.actions.push(action); applyAction(action, result); } // In non-interactive mode, proceed if no errors were rejected result.proceed = result.rejected.length === 0; return result; } /** * Handle approval for a single item */ async function handleItemApproval( item: ApprovalItem, options: ApprovalOptions, isError: boolean ): Promise<ApprovalAction> { console.log(`\n📄 ${item.filePath}`); console.log(` Severity: ${item.severity.toUpperCase()}`); console.log(` Confidence: ${(item.confidence * 100).toFixed(1)}%`); console.log(` Location Valid: ${item.allowedInLocation ? '✅' : '❌'}`); // Display issues console.log('\n Issues:'); for (const issue of item.issues) { console.log(` - ${issue.severity.toUpperCase()}: ${issue.message}`); if (issue.line) { console.log(` Line ${issue.line}`); } if (issue.context) { const contextLines = issue.context.split('\n').slice(0, 3); console.log(` Context: ${contextLines.join(' | ')}`); } } // Display suggestions if (item.suggestions.length > 0) { console.log('\n Suggestions:'); for (let i = 0; i < item.suggestions.length; i++) { console.log(` ${i + 1}. ${item.suggestions[i]}`); } } // Get user choice const choices = buildChoices(item, isError); const choice = await getUserChoice(choices, options); return processChoice(choice, item, options); } /** * Build available choices for user */ function buildChoices( item: ApprovalItem, isError: boolean ): Array<{ key: string; description: string; available: boolean }> { const choices = [ { key: 'a', description: 'Approve (include in commit)', available: !isError }, { key: 'r', description: 'Reject (exclude from commit)', available: true }, { key: 'i', description: 'Ignore (add to .gitignore)', available: true }, { key: 'm', description: 'Move to different location', available: !item.allowedInLocation }, { key: 'v', description: 'View file content', available: true }, { key: 'e', description: 'Edit file', available: true }, { key: 's', description: 'Skip for now', available: false }, // Not implemented yet { key: 'h', description: 'Show help', available: true }, ]; return choices; } /** * Get user choice */ async function getUserChoice( choices: Array<{ key: string; description: string; available: boolean }>, options: ApprovalOptions ): Promise<string> { const availableChoices = choices.filter(c => c.available); console.log('\n Available actions:'); for (const choice of availableChoices) { console.log(` [${choice.key}] ${choice.description}`); } if (options.batchMode) { console.log(' [all] Apply to all similar files'); } const rl = createInterface({ input: process.stdin, output: process.stdout, }); return new Promise(resolve => { const askQuestion = () => { rl.question('\n Your choice: ', answer => { const choice = answer.trim().toLowerCase(); if (choice === 'h') { displayHelp(); askQuestion(); } else if (availableChoices.some(c => c.key === choice) || choice === 'all') { rl.close(); resolve(choice); } else { console.log(' Invalid choice. Please try again.'); askQuestion(); } }); }; askQuestion(); }); } /** * Process user choice into action */ function processChoice( choice: string, item: ApprovalItem, options: ApprovalOptions ): ApprovalAction { switch (choice) { case 'a': return { type: 'approve', filePath: item.filePath }; case 'r': return { type: 'reject', filePath: item.filePath, reason: 'User rejected' }; case 'i': return { type: 'ignore', filePath: item.filePath }; case 'm': { const targetDir = getMoveTarget(item); return { type: 'move', filePath: item.filePath, target: targetDir }; } case 'v': viewFileContent(item.filePath); return processChoice(choice, item, options); // Re-prompt after viewing case 'e': editFile(item.filePath); return processChoice(choice, item, options); // Re-prompt after editing default: return { type: 'reject', filePath: item.filePath, reason: 'Invalid choice' }; } } /** * Get move target from suggestions */ function getMoveTarget(item: ApprovalItem): string { const moveSuggestions = item.suggestions.filter(s => s.includes('Move') || s.includes('move')); if (moveSuggestions.length > 0) { console.log('\n Suggested locations:'); for (let i = 0; i < moveSuggestions.length; i++) { console.log(` ${i + 1}. ${moveSuggestions[i]}`); } // For now, extract first suggested directory const firstSuggestion = moveSuggestions[0]; if (firstSuggestion) { const dirMatch = firstSuggestion.match(/(?:to|in)\s+(\w+\/)/); return dirMatch ? dirMatch[1]! : 'scripts/'; } } return 'scripts/'; // Default fallback } /** * View file content */ function viewFileContent(filePath: string): void { try { const fs = require('fs'); const content = fs.readFileSync(filePath, 'utf8'); const lines = content.split('\n'); console.log(`\n Content of ${filePath}:`); console.log(' ' + '='.repeat(50)); // Show first 20 lines for (let i = 0; i < Math.min(20, lines.length); i++) { console.log(` ${(i + 1).toString().padStart(3)}: ${lines[i]}`); } if (lines.length > 20) { console.log(` ... (${lines.length - 20} more lines)`); } console.log(' ' + '='.repeat(50)); } catch (error) { console.log(` Error reading file: ${error instanceof Error ? error.message : String(error)}`); } } /** * Edit file (placeholder - would open in editor) */ function editFile(filePath: string): void { console.log(`\n Opening ${filePath} in editor...`); console.log(' (This would open your default editor in a real implementation)'); console.log(' Press Enter to continue after editing...'); // In a real implementation, this would: // 1. Open the file in the user's default editor // 2. Wait for the editor to close // 3. Re-analyze the file for issues } /** * Apply action to result */ function applyAction(action: ApprovalAction, result: ApprovalResult): void { switch (action.type) { case 'approve': result.approved.push(action.filePath); break; case 'reject': result.rejected.push(action.filePath); break; case 'move': if (action.target) { result.moved.push({ from: action.filePath, to: action.target }); } break; case 'ignore': result.ignored.push(action.filePath); break; } } /** * Display approval summary */ function displayApprovalSummary(items: ApprovalItem[]): void { console.log('\n' + '='.repeat(60)); console.log('🔍 SMART GIT PUSH - VALIDATION RESULTS'); console.log('='.repeat(60)); const errorCount = items.filter(item => item.severity === 'error').length; const warningCount = items.filter(item => item.severity === 'warning').length; const infoCount = items.filter(item => item.severity === 'info').length; console.log(`📊 Summary: ${items.length} files with issues`); console.log(` 🚨 Errors: ${errorCount}`); console.log(` ⚠️ Warnings: ${warningCount}`); console.log(` 💡 Info: ${infoCount}`); if (errorCount > 0) { console.log('\n❌ Files with errors must be resolved before proceeding'); } console.log('\n📋 Review each file and choose an action:'); } /** * Get final confirmation */ async function getFinalConfirmation( result: ApprovalResult, options: ApprovalOptions ): Promise<boolean> { console.log('\n' + '='.repeat(60)); console.log('📋 FINAL SUMMARY'); console.log('='.repeat(60)); console.log(`✅ Approved: ${result.approved.length} files`); console.log(`❌ Rejected: ${result.rejected.length} files`); console.log(`📁 To Move: ${result.moved.length} files`); console.log(`🙈 To Ignore: ${result.ignored.length} files`); if (result.approved.length > 0) { console.log('\n✅ Files to be committed:'); for (const file of result.approved) { console.log(` - ${file}`); } } if (result.rejected.length > 0) { console.log('\n❌ Files excluded from commit:'); for (const file of result.rejected) { console.log(` - ${file}`); } } if (result.moved.length > 0) { console.log('\n📁 Files to be moved:'); for (const move of result.moved) { console.log(` - ${move.from} → ${move.to}`); } } if (options.dryRun) { console.log('\n🔍 DRY RUN - No actual changes will be made'); return true; } const rl = createInterface({ input: process.stdin, output: process.stdout, }); return new Promise(resolve => { rl.question('\n🚀 Proceed with git push? (y/N): ', answer => { rl.close(); const proceed = answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes'; resolve(proceed); }); }); } /** * Display help */ function displayHelp(): void { console.log('\n' + '='.repeat(40)); console.log('📖 HELP'); console.log('='.repeat(40)); console.log('Actions:'); console.log(' [a] Approve - Include file in commit'); console.log(' [r] Reject - Exclude file from commit'); console.log(' [i] Ignore - Add file to .gitignore'); console.log(' [m] Move - Move file to appropriate location'); console.log(' [v] View - Show file content'); console.log(' [e] Edit - Open file in editor'); console.log(' [h] Help - Show this help'); console.log(''); console.log('Tips:'); console.log(' - Files with errors must be resolved before proceeding'); console.log(' - Use [m] to move files to proper directories'); console.log(' - Use [i] to permanently ignore temporary files'); console.log(' - Use [v] to examine file content before deciding'); console.log('='.repeat(40)); } /** * Batch approval for similar files */ export function batchApproval( items: ApprovalItem[], action: ApprovalAction, criteria: { sameSeverity?: boolean; sameIssueType?: boolean; samePattern?: boolean; } ): ApprovalAction[] { const actions: ApprovalAction[] = []; const referenceItem = items.find(item => item.filePath === action.filePath); if (!referenceItem) { return [action]; } for (const item of items) { let matches = true; if (criteria.sameSeverity && item.severity !== referenceItem.severity) { matches = false; } if (criteria.sameIssueType) { const itemTypes = item.issues.map(i => i.type); const refTypes = referenceItem.issues.map(i => i.type); if (!itemTypes.some(type => refTypes.includes(type))) { matches = false; } } if (matches) { actions.push({ type: action.type, filePath: item.filePath, target: action.target, reason: `Batch ${action.type} - similar to ${action.filePath}`, }); } } return actions; } /** * Save approval preferences for future use */ export async function saveApprovalPreferences( actions: ApprovalAction[], configPath: string = '.smartgit-approvals.json' ): Promise<void> { try { const preferences = { timestamp: new Date().toISOString(), actions: actions.map(action => ({ pattern: action.filePath.replace(/[^/]+$/, '*'), // Replace filename with wildcard type: action.type, reason: action.reason, })), }; writeFileSync(configPath, JSON.stringify(preferences, null, 2)); console.log(`\n💾 Approval preferences saved to ${configPath}`); } catch (error) { console.log( `\n⚠️ Could not save preferences: ${error instanceof Error ? error.message : String(error)}` ); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tosin2013/mcp-adr-analysis-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server