UNPKG

@nanocollective/nanocoder

Version:

A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter

266 lines 9.44 kB
import { existsSync, readFileSync } from 'fs'; import { join } from 'path'; export class ExistingRulesExtractor { projectPath; skipAgentsMd; static AI_CONFIG_FILES = [ // AI agent specific files 'AGENTS.md', 'AGENT.md', 'CLAUDE.md', 'GEMINI.md', 'CURSOR.md', // Rules files in various directories '.cursor/rules', '.clinerules/rules', '.roorules/rules', // Alternative locations '.ai/rules', '.ai/instructions', 'ai-instructions.md', 'coding-guidelines.md', 'dev-guidelines.md', ]; constructor(projectPath, skipAgentsMd = false) { this.projectPath = projectPath; this.skipAgentsMd = skipAgentsMd; } /** * Find and extract content from existing AI configuration files */ extractExistingRules() { const found = []; for (const configFile of ExistingRulesExtractor.AI_CONFIG_FILES) { // Skip AGENTS.md when force regenerating if (this.skipAgentsMd && configFile === 'AGENTS.md') { continue; } const filePath = join(this.projectPath, configFile); if (existsSync(filePath)) { try { const content = readFileSync(filePath, 'utf-8'); const cleanContent = this.cleanAndExtractRelevantContent(content, configFile); if (cleanContent.trim()) { found.push({ source: configFile, content: cleanContent, type: this.determineFileType(configFile), }); } } catch { // Skip files we can't read continue; } } } return found; } /** * Clean content and extract only AI-relevant information */ cleanAndExtractRelevantContent(content, filename) { // Remove excessive markdown formatting but keep structure let cleaned = content; // Remove multiple consecutive empty lines cleaned = cleaned.replace(/\n\s*\n\s*\n/g, '\n\n'); // Remove excessive header decoration cleaned = cleaned.replace(/#{4,}/g, '###'); // For specific file types, extract relevant sections if (filename.toLowerCase().includes('claude') || filename.toLowerCase().includes('agents')) { return this.extractAIAgentSections(cleaned); } if (filename.includes('rules')) { return this.extractRulesSections(cleaned); } // For generic files, extract key sections return this.extractGeneralInstructions(cleaned); } /** * Extract AI agent specific sections */ extractAIAgentSections(content) { const relevantSections = []; const lines = content.split('\n'); let currentSection = ''; let inRelevantSection = false; let sectionHeader = ''; const relevantHeaders = [ 'coding', 'style', 'convention', 'pattern', 'architecture', 'testing', 'security', 'performance', 'build', 'deployment', 'project', 'structure', 'guidelines', 'rules', 'instructions', 'important', 'note', 'requirement', 'constraint', ]; for (const line of lines) { const trimmed = line.trim().toLowerCase(); // Check if this is a header if (line.match(/^#+\s/) || line.match(/^[=-]{3,}$/)) { // Save previous section if it was relevant if (inRelevantSection && currentSection.trim()) { relevantSections.push(sectionHeader + '\n' + currentSection.trim()); } // Check if new section is relevant inRelevantSection = relevantHeaders.some(keyword => trimmed.includes(keyword)); sectionHeader = line; currentSection = ''; } else if (inRelevantSection) { currentSection += line + '\n'; } } // Don't forget the last section if (inRelevantSection && currentSection.trim()) { relevantSections.push(sectionHeader + '\n' + currentSection.trim()); } // If no specific sections found, extract key paragraphs if (relevantSections.length === 0) { return this.extractKeyParagraphs(content); } return relevantSections.join('\n\n'); } /** * Extract rules file content */ extractRulesSections(content) { // Rules files are typically more concise, keep most content const lines = content.split('\n'); const filtered = lines.filter(line => { const trimmed = line.trim(); // Skip very generic lines if (trimmed.length < 10) return true; // Keep short lines for structure if (trimmed.includes('example') && trimmed.includes('only')) return false; if (trimmed.includes('this is just') && trimmed.includes('example')) return false; return true; }); return filtered.join('\n'); } /** * Extract general instructions from any file */ extractGeneralInstructions(content) { return this.extractKeyParagraphs(content); } /** * Extract key paragraphs based on content analysis */ extractKeyParagraphs(content) { const paragraphs = content.split('\n\n').filter(p => p.trim().length > 50); const keyParagraphs = []; const importantKeywords = [ 'must', 'should', 'always', 'never', 'important', 'critical', 'required', 'mandatory', 'essential', 'convention', 'pattern', 'style', 'format', 'structure', 'architecture', 'test', 'security', ]; for (const paragraph of paragraphs) { const lowerParagraph = paragraph.toLowerCase(); const relevanceScore = importantKeywords.reduce((score, keyword) => { return score + (lowerParagraph.includes(keyword) ? 1 : 0); }, 0); // Include paragraphs with at least 2 important keywords if (relevanceScore >= 2) { keyParagraphs.push(paragraph.trim()); } } // If we didn't find enough relevant content, take first few paragraphs if (keyParagraphs.length === 0 && paragraphs.length > 0) { return paragraphs.slice(0, 3).join('\n\n'); } return keyParagraphs.slice(0, 5).join('\n\n'); // Limit to 5 paragraphs max } /** * Determine the type of configuration file */ determineFileType(filename) { const lower = filename.toLowerCase(); if (lower.includes('agent') || lower.includes('claude') || lower.includes('gemini')) { return 'agents'; } if (lower.includes('rule') || lower.includes('.cursor/') || lower.includes('.clinerules/')) { return 'rules'; } return 'instructions'; } /** * Merge existing rules into a single consolidated section */ static mergeExistingRules(existingRules) { if (existingRules.length === 0) { return ''; } const sections = []; sections.push('## Existing Project Guidelines'); sections.push(''); sections.push('*The following guidelines were found in existing AI configuration files:*'); sections.push(''); // Group by type const agentRules = existingRules.filter(r => r.type === 'agents'); const ruleFiles = existingRules.filter(r => r.type === 'rules'); const instructions = existingRules.filter(r => r.type === 'instructions'); // Add agent-specific rules if (agentRules.length > 0) { sections.push('### AI Agent Guidelines'); for (const rule of agentRules) { sections.push(`**From ${rule.source}:**`); sections.push(rule.content); sections.push(''); } } // Add rule files if (ruleFiles.length > 0) { sections.push('### Project Rules'); for (const rule of ruleFiles) { sections.push(`**From ${rule.source}:**`); sections.push(rule.content); sections.push(''); } } // Add general instructions if (instructions.length > 0) { sections.push('### Additional Instructions'); for (const rule of instructions) { sections.push(`**From ${rule.source}:**`); sections.push(rule.content); sections.push(''); } } return sections.join('\n'); } } //# sourceMappingURL=existing-rules-extractor.js.map