neandoc
Version:
AI-powered CLI tool for automatic code documentation. Primal Code. Modern Docs.
322 lines (259 loc) • 11.1 kB
JavaScript
const chalk = require('chalk');
class NeandocAssistant {
constructor(options = {}) {
this.checkInterval = options.checkInterval || 300000; // 5 minutes
this.watchMode = options.watchMode || false;
this.daemonMode = options.daemonMode || false;
this.lastPromptTime = null;
this.isRunning = false;
this.watchInterval = null; // Store interval reference for proper cleanup
}
async analyzeAndPrompt(codeStructure) {
const analysis = this.analyzeDocumentationGaps(codeStructure);
if (analysis.hasMissingDocs) {
const prompt = this.generateClaudePrompt(analysis, codeStructure);
this.sendPromptToClaude(prompt);
return analysis;
}
console.log(chalk.green('✅ All code is properly documented!'));
return analysis;
}
analyzeDocumentationGaps(codeStructure) {
const gaps = {
missingTechnical: [],
missingSimple: [],
hasMissingDocs: false,
totalElements: 0
};
// Check functions
for (const func of codeStructure.functions) {
gaps.totalElements++;
const hasDocumentation = this.checkExistingDocumentation(codeStructure.content, func);
if (!hasDocumentation.technical) {
gaps.missingTechnical.push(func);
}
if (!hasDocumentation.simple) {
gaps.missingSimple.push(func);
}
}
// Check classes
for (const cls of codeStructure.classes) {
gaps.totalElements++;
const hasDocumentation = this.checkExistingDocumentation(codeStructure.content, cls);
if (!hasDocumentation.technical) {
gaps.missingTechnical.push(cls);
}
if (!hasDocumentation.simple) {
gaps.missingSimple.push(cls);
}
}
gaps.hasMissingDocs = gaps.missingTechnical.length > 0 || gaps.missingSimple.length > 0;
return gaps;
}
checkExistingDocumentation(content, element) {
const lines = content.split('\n');
const elementLine = element.lineNumber - 1;
// Check 10 lines before the element for existing comments
const checkStart = Math.max(0, elementLine - 10);
const contextLines = lines.slice(checkStart, elementLine).join('\n');
const hasTechnical = /\/\*\*[\s\S]*?Technische Erklärung|Technical Explanation/i.test(contextLines);
const hasSimple = /\/\*\*[\s\S]*?Einfache Erklärung|Simple Explanation/i.test(contextLines);
return {
technical: hasTechnical,
simple: hasSimple,
hasAny: hasTechnical || hasSimple
};
}
generateClaudePrompt(analysis, codeStructure) {
const prompt = `🦣 **NEANDOC DOCUMENTATION REQUEST**
Du bist ein Code-Dokumentations-Experte. Folgende Elemente brauchen Kommentare:
`;
let promptContent = prompt;
// Add missing technical documentation
if (analysis.missingTechnical.length > 0) {
promptContent += '## 🔧 TECHNISCHE KOMMENTARE FEHLEN:\n\n';
for (const element of analysis.missingTechnical) {
const context = this.extractCodeContext(codeStructure.content, element);
promptContent += `### ${element.name} (${element.type})\n`;
promptContent += `**Datei:** ${codeStructure.filePath}:${element.lineNumber}\n`;
promptContent += `**Code:**\n\`\`\`${codeStructure.extension.slice(1)}\n${context}\n\`\`\`\n\n`;
}
}
// Add missing simple explanations
if (analysis.missingSimple.length > 0) {
promptContent += '## 👥 EINFACHE ERKLÄRUNGEN FEHLEN:\n\n';
for (const element of analysis.missingSimple) {
promptContent += `- ${element.name} (${element.type}) in ${codeStructure.filePath}:${element.lineNumber}\n`;
}
}
promptContent += '\n## 📋 AUFGABE:\n\n';
promptContent += 'Erstelle für jedes Element oben:\n';
promptContent += '1. **Technische Erklärung** für Entwickler\n';
promptContent += '2. **Einfache Erklärung** für Laien (Alltagsvergleiche)\n\n';
promptContent += '**Format:**\n```\n/**\n * Technische Erklärung:\n * [Detaillierte tech. Beschreibung]\n *\n * Einfache Erklärung:\n * [Alltagsvergleich für Laien]\n */\n```\n\n';
promptContent += '**Wichtig:** Füge die Kommentare direkt über die entsprechenden Funktionen/Klassen ein!';
return promptContent;
}
sendPromptToClaude(prompt) {
console.log(chalk.blue('\n🤖 NEANDOC → CLAUDE'));
console.log(chalk.gray('=' + '='.repeat(50)));
console.log(prompt);
console.log(chalk.gray('=' + '='.repeat(50)));
console.log(chalk.yellow('\n📋 Copy the prompt above and send it to Claude!'));
console.log(chalk.cyan('💡 Then run: npx neandoc --apply <claude-response-file>'));
this.lastPromptTime = Date.now();
}
extractCodeContext(content, element, contextLines = 5) {
const lines = content.split('\n');
const elementLine = element.lineNumber - 1;
const start = Math.max(0, elementLine - contextLines);
const end = Math.min(lines.length, elementLine + contextLines + 10);
return lines.slice(start, end).join('\n');
}
async startWatchMode(directory) {
if (this.isRunning) {
console.log(chalk.yellow('⚠️ Watch mode already running'));
return;
}
this.isRunning = true;
console.log(chalk.blue(`🦣 Neandoc watching ${directory} for documentation gaps...`));
console.log(chalk.gray(`📅 Checking every ${this.checkInterval / 1000} seconds`));
// Initial check
await this.performCheck(directory);
// Set up interval with proper error handling
this.watchInterval = setInterval(async () => {
if (!this.isRunning) {
this.stopWatchMode();
return;
}
try {
await this.performCheck(directory);
} catch (error) {
console.error(chalk.red(`❌ Watch check failed: ${error.message}`));
// Continue running on errors, don't crash the watcher
}
}, this.checkInterval);
// Handle graceful shutdown
const shutdownHandler = () => {
console.log(chalk.yellow('\n⏹️ Stopping Neandoc watch mode...'));
this.stopWatchMode();
process.exit(0);
};
process.on('SIGINT', shutdownHandler);
process.on('SIGTERM', shutdownHandler);
}
stopWatchMode() {
if (this.watchInterval) {
clearInterval(this.watchInterval);
this.watchInterval = null;
}
this.isRunning = false;
}
async performCheck(directory) {
try {
const timestamp = new Date().toLocaleTimeString();
console.log(chalk.gray(`\n[${timestamp}] 🔍 Checking documentation...`));
const Parser = require('./parser');
const parser = new Parser();
// Find and analyze files
const glob = require('glob');
const path = require('path');
const patterns = ['**/*.js', '**/*.ts', '**/*.jsx', '**/*.tsx', '**/*.py'];
let totalGaps = 0;
const allAnalyses = [];
for (const pattern of patterns) {
const files = await glob.glob(path.join(directory, pattern), {
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**']
});
for (const file of files) {
const codeStructure = await parser.parseFile(file);
const analysis = this.analyzeDocumentationGaps(codeStructure);
if (analysis.hasMissingDocs) {
totalGaps += analysis.missingTechnical.length + analysis.missingSimple.length;
allAnalyses.push({ file, analysis, codeStructure });
}
}
}
if (totalGaps > 0) {
console.log(chalk.red(`❌ Found ${totalGaps} documentation gaps!`));
// Generate comprehensive prompt for all gaps
if (allAnalyses.length > 0) {
const comprehensivePrompt = this.generateComprehensivePrompt(allAnalyses);
this.sendPromptToClaude(comprehensivePrompt);
}
} else {
console.log(chalk.green('✅ All code properly documented!'));
}
} catch (error) {
console.error(chalk.red(`❌ Check failed: ${error.message}`));
}
}
generateComprehensivePrompt(analyses) {
let prompt = '🦣 **NEANDOC COMPREHENSIVE DOCUMENTATION REQUEST**\n\n';
prompt += 'Du bist Code-Dokumentations-Experte. Mehrere Dateien brauchen Kommentare:\n\n';
for (const { file, analysis, codeStructure } of analyses) {
if (analysis.hasMissingDocs) {
prompt += `## 📁 ${file}\n\n`;
if (analysis.missingTechnical.length > 0) {
prompt += '**Fehlende technische Kommentare:**\n';
for (const element of analysis.missingTechnical) {
const context = this.extractCodeContext(codeStructure.content, element, 3);
prompt += `### ${element.name} (${element.type}) - Line ${element.lineNumber}\n`;
prompt += `\`\`\`${codeStructure.extension.slice(1)}\n${context}\n\`\`\`\n\n`;
}
}
if (analysis.missingSimple.length > 0) {
prompt += `**Fehlende einfache Erklärungen:** ${analysis.missingSimple.map(e => e.name).join(', ')}\n\n`;
}
}
}
prompt += '## 📋 AUFGABE:\n\n';
prompt += 'Für jedes Element:\n';
prompt += '1. **Technische Erklärung** für Entwickler\n';
prompt += '2. **Einfache Erklärung** für Laien\n\n';
prompt += '**Format:** Füge Kommentare direkt über die Funktionen ein:\n';
prompt += '```\n/**\n * Technische Erklärung: [Details]\n * Einfache Erklärung: [Alltagsvergleich]\n */\n```\n\n';
prompt += '**Wichtig:** Bearbeite alle Dateien vollständig!';
return prompt;
}
buildPrompt(codeStructure, prompts) {
const basePrompt = prompts.basePrompt || `Du bist Neandoc, ein AI-Assistent, der Code so kommentiert und erklärt, dass ihn sowohl Entwickler als auch Laien verstehen.
Deine Aufgaben für jeden Codeabschnitt (Funktion, Klasse, Block):
1. Schreibe eine **technische Erklärung** für Entwickler.
2. Schreibe eine **einfache Erklärung** in Alltagssprache.
Nutze folgendes Format:
/**
* Technische Erklärung:
* [...]
*
* Einfache Erklärung:
* [...]
*/
Der Code selbst darf nicht verändert werden.`;
return basePrompt;
}
extractContext(content, startIndex, contextSize = 200) {
const start = Math.max(0, startIndex - contextSize);
const end = Math.min(content.length, startIndex + contextSize);
return content.substring(start, end);
}
async isMCPAvailable() {
// Check if MCP server is available
// TODO: Implement actual MCP server availability check
return false; // For now, assume not available
}
async callMCPProtocol() {
// Implement MCP protocol communication
throw new Error('MCP protocol not yet implemented');
}
// Mock responses for development
async mockClaudeResponse() {
// TODO: Implement mock response generation
return { comments: [] };
}
async mockOpenAIResponse() {
// TODO: Implement mock OpenAI response generation
return { comments: [] };
}
}
module.exports = NeandocAssistant;