UNPKG

automagik-genie

Version:

Self-evolving AI agent orchestration framework with Model Context Protocol support

482 lines (481 loc) 19.6 kB
"use strict"; /** * Genesis Diff Generator - Non-destructive framework update comparison * * Compares LOCAL workspace files vs UPSTREAM package template * WITHOUT copying or modifying any files. Pure information generation. * * This is the foundation for agent-driven framework updates. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateGenesisDiff = generateGenesisDiff; const path_1 = __importDefault(require("path")); const fs_1 = require("fs"); const fs_utils_1 = require("./fs-utils"); /** * Generate genesis diff between workspace and upstream package template * * NO DESTRUCTIVE OPERATIONS - only reads files and generates report * * @param workspacePath - User's project root * @param packageRoot - Genie package installation directory * @param oldVersion - Current installed version * @param newVersion - New package version * @returns Diff result with path to generated diff file */ async function generateGenesisDiff(workspacePath, packageRoot, oldVersion, newVersion) { const diffId = (0, fs_utils_1.toIsoId)(); const diffFileName = `v${oldVersion.replace(/\./g, '-')}-to-v${newVersion.replace(/\./g, '-')}.diff.md`; // Store in .genie/upgrades/ (new location for upgrade diffs) const upgradesDir = path_1.default.join(workspacePath, '.genie', 'upgrades'); await (0, fs_utils_1.ensureDir)(upgradesDir); const diffPath = path_1.default.join(upgradesDir, diffFileName); console.log(' Comparing framework files...'); // Collect files from both locations const localFiles = await collectLocalKnowledgeFiles(workspacePath); const upstreamFiles = await collectUpstreamKnowledgeFiles(packageRoot); console.log(` Local: ${localFiles.size} files | Upstream: ${upstreamFiles.size} files`); // Compare files const diffs = await compareFilesDirectly(localFiles, upstreamFiles, workspacePath, packageRoot); // Generate detailed diff report with actual content diffs const report = await buildGenesisDiffReport(diffs, oldVersion, newVersion, diffId); await fs_1.promises.writeFile(diffPath, report, 'utf8'); const summary = { added: diffs.filter(d => d.status === 'added').length, removed: diffs.filter(d => d.status === 'removed').length, modified: diffs.filter(d => d.status === 'modified').length, unchanged: diffs.filter(d => d.status === 'unchanged').length }; const hasChanges = summary.added > 0 || summary.removed > 0 || summary.modified > 0; return { diffPath, diffId, summary, hasChanges }; } /** * Collect knowledge files from LOCAL workspace * These are the user's current files */ async function collectLocalKnowledgeFiles(workspacePath) { const files = new Map(); // Root documentation files const rootDocs = ['AGENTS.md', 'CLAUDE.md']; for (const doc of rootDocs) { const docPath = path_1.default.join(workspacePath, doc); if (await (0, fs_utils_1.pathExists)(docPath)) { files.set(doc, docPath); } } // .genie folder (excluding user content) const geniePath = path_1.default.join(workspacePath, '.genie'); if (await (0, fs_utils_1.pathExists)(geniePath)) { const knowledgeDirs = [ 'agents', 'spells', 'workflows', 'product', 'code', 'create', 'neurons', 'qa', 'utilities', 'scripts' ]; for (const dir of knowledgeDirs) { const dirPath = path_1.default.join(geniePath, dir); if (await (0, fs_utils_1.pathExists)(dirPath)) { const dirFiles = await (0, fs_utils_1.collectFiles)(dirPath, { filter: (relPath) => { // Exclude user-specific content const excludeDirs = ['wishes', 'reports', 'state', 'backups', 'upgrades']; const firstSeg = relPath.split(path_1.default.sep)[0]; return !excludeDirs.includes(firstSeg); } }); for (const file of dirFiles) { const relPath = path_1.default.join('.genie', dir, file); const absPath = path_1.default.join(dirPath, file); files.set(relPath, absPath); } } } // Also check for AGENTS.md and other root files in .genie/ const genieRootFiles = ['AGENTS.md', 'README.md']; for (const file of genieRootFiles) { const filePath = path_1.default.join(geniePath, file); if (await (0, fs_utils_1.pathExists)(filePath)) { files.set(path_1.default.join('.genie', file), filePath); } } } return files; } /** * Collect knowledge files from UPSTREAM package template * These are the new framework files from the package */ async function collectUpstreamKnowledgeFiles(packageRoot) { const files = new Map(); // Root documentation files in package const rootDocs = ['AGENTS.md', 'CLAUDE.md']; for (const doc of rootDocs) { const docPath = path_1.default.join(packageRoot, doc); if (await (0, fs_utils_1.pathExists)(docPath)) { files.set(doc, docPath); } } // .genie folder from package const geniePath = path_1.default.join(packageRoot, '.genie'); if (await (0, fs_utils_1.pathExists)(geniePath)) { const knowledgeDirs = [ 'agents', 'spells', 'workflows', 'product', 'code', 'create', 'neurons', 'qa', 'utilities', 'scripts' ]; for (const dir of knowledgeDirs) { const dirPath = path_1.default.join(geniePath, dir); if (await (0, fs_utils_1.pathExists)(dirPath)) { const dirFiles = await (0, fs_utils_1.collectFiles)(dirPath, { filter: (relPath) => { // Exclude user-specific directories (these shouldn't exist in package anyway) const excludeDirs = ['wishes', 'reports', 'state', 'backups', 'upgrades']; const firstSeg = relPath.split(path_1.default.sep)[0]; return !excludeDirs.includes(firstSeg); } }); for (const file of dirFiles) { const relPath = path_1.default.join('.genie', dir, file); const absPath = path_1.default.join(dirPath, file); files.set(relPath, absPath); } } } // Root files in .genie/ const genieRootFiles = ['AGENTS.md', 'README.md']; for (const file of genieRootFiles) { const filePath = path_1.default.join(geniePath, file); if (await (0, fs_utils_1.pathExists)(filePath)) { files.set(path_1.default.join('.genie', file), filePath); } } } return files; } /** * Compare files directly between local workspace and upstream package * No copying - just read and compare */ async function compareFilesDirectly(localFiles, upstreamFiles, workspacePath, packageRoot) { const diffs = []; const allPaths = new Set([...localFiles.keys(), ...upstreamFiles.keys()]); for (const relPath of allPaths) { const localPath = localFiles.get(relPath); const upstreamPath = upstreamFiles.get(relPath); if (!localPath && upstreamPath) { // File exists in upstream but not in local (new file) const stats = await fs_1.promises.stat(upstreamPath); const content = await fs_1.promises.readFile(upstreamPath, 'utf8'); diffs.push({ path: relPath, status: 'added', newSize: stats.size, newContent: content }); } else if (localPath && !upstreamPath) { // File exists in local but not in upstream (removed from framework) const stats = await fs_1.promises.stat(localPath); const content = await fs_1.promises.readFile(localPath, 'utf8'); diffs.push({ path: relPath, status: 'removed', oldSize: stats.size, oldContent: content }); } else if (localPath && upstreamPath) { // File exists in both - check if modified const [localContent, upstreamContent] = await Promise.all([ fs_1.promises.readFile(localPath, 'utf8'), fs_1.promises.readFile(upstreamPath, 'utf8') ]); const [localStats, upstreamStats] = await Promise.all([ fs_1.promises.stat(localPath), fs_1.promises.stat(upstreamPath) ]); if (localContent !== upstreamContent) { diffs.push({ path: relPath, status: 'modified', oldSize: localStats.size, newSize: upstreamStats.size, oldContent: localContent, newContent: upstreamContent }); } else { diffs.push({ path: relPath, status: 'unchanged', oldSize: localStats.size, newSize: upstreamStats.size }); } } } return diffs.sort((a, b) => a.path.localeCompare(b.path)); } /** * Build detailed genesis diff report with actual content changes */ async function buildGenesisDiffReport(diffs, oldVersion, newVersion, diffId) { const added = diffs.filter(d => d.status === 'added'); const removed = diffs.filter(d => d.status === 'removed'); const modified = diffs.filter(d => d.status === 'modified'); const lines = []; lines.push(`# Genie Framework Upgrade Diff`); lines.push(``); lines.push(`**Upgrade:** v${oldVersion} → v${newVersion}`); lines.push(`**Generated:** ${new Date().toISOString()}`); lines.push(`**Diff ID:** ${diffId}`); lines.push(``); lines.push(`---`); lines.push(``); lines.push(`## Summary`); lines.push(``); lines.push(`| Type | Count |`); lines.push(`|------|-------|`); lines.push(`| Added | ${added.length} |`); lines.push(`| Removed | ${removed.length} |`); lines.push(`| Modified | ${modified.length} |`); lines.push(`| **Total Changes** | **${added.length + removed.length + modified.length}** |`); lines.push(``); // Added files (new in upstream) if (added.length > 0) { lines.push(`## New Files (${added.length})`); lines.push(``); lines.push(`These files exist in the new version but not in your workspace:`); lines.push(``); for (const diff of added) { const size = diff.newSize ? ` (${formatBytes(diff.newSize)})` : ''; lines.push(`### ✅ \`${diff.path}\`${size}`); lines.push(``); if (diff.newContent && diff.newContent.length < 10000) { lines.push(`<details>`); lines.push(`<summary>View new file content</summary>`); lines.push(``); lines.push('```markdown'); lines.push(diff.newContent.trim()); lines.push('```'); lines.push(``); lines.push(`</details>`); lines.push(``); } else { lines.push(`*File too large to include inline. Review directly.*`); lines.push(``); } } } // Removed files (exist locally but removed from framework) if (removed.length > 0) { lines.push(`## Removed from Framework (${removed.length})`); lines.push(``); lines.push(`These files exist in your workspace but are no longer part of the framework:`); lines.push(``); for (const diff of removed) { const size = diff.oldSize ? ` (${formatBytes(diff.oldSize)})` : ''; lines.push(`- ❌ \`${diff.path}\`${size}`); } lines.push(``); lines.push(`**Action:** Review if these are user customizations to keep or obsolete files to remove.`); lines.push(``); } // Modified files (most important - these need careful review) if (modified.length > 0) { lines.push(`## Modified Files (${modified.length})`); lines.push(``); lines.push(`These files have changed in the upstream framework:`); lines.push(``); for (const diff of modified) { const oldSize = diff.oldSize ? formatBytes(diff.oldSize) : '?'; const newSize = diff.newSize ? formatBytes(diff.newSize) : '?'; const delta = diff.oldSize && diff.newSize ? ` (${diff.newSize > diff.oldSize ? '+' : ''}${formatBytes(diff.newSize - diff.oldSize)})` : ''; lines.push(`### 📝 \`${diff.path}\``); lines.push(``); lines.push(`**Size:** ${oldSize}${newSize}${delta}`); lines.push(``); // Generate unified diff for content if (diff.oldContent && diff.newContent) { const unifiedDiff = generateUnifiedDiff(diff.path, diff.oldContent, diff.newContent); if (unifiedDiff.length < 15000) { lines.push(`<details>`); lines.push(`<summary>View changes</summary>`); lines.push(``); lines.push('```diff'); lines.push(unifiedDiff); lines.push('```'); lines.push(``); lines.push(`</details>`); lines.push(``); } else { lines.push(`*Diff too large to include inline. Key changes summarized below.*`); lines.push(``); // Provide a summary instead const addedLines = diff.newContent.split('\n').length - diff.oldContent.split('\n').length; lines.push(`**Line changes:** ${addedLines >= 0 ? '+' : ''}${addedLines} lines`); lines.push(``); } } } } lines.push(`---`); lines.push(``); lines.push(`## Agent Instructions`); lines.push(``); lines.push(`This diff file documents all framework changes from v${oldVersion} to v${newVersion}.`); lines.push(``); lines.push(`**Your task:**`); lines.push(`1. **Learn** the new patterns and teachings from added/modified files`); lines.push(`2. **Apply** necessary changes to workspace (preserve user customizations)`); lines.push(`3. **Report** what was learned and what actions were taken`); lines.push(``); lines.push(`**Important:**`); lines.push(`- Do NOT blindly copy files - understand the intent of each change`); lines.push(`- Preserve user customizations in workspace files`); lines.push(`- For conflicts, present options to user`); lines.push(`- Create a report of learnings applied`); lines.push(``); return lines.join('\n'); } /** * Generate unified diff format for content comparison */ function generateUnifiedDiff(filename, oldContent, newContent) { const oldLines = oldContent.split('\n'); const newLines = newContent.split('\n'); // Simple diff - show changed lines with context const result = []; result.push(`--- a/${filename}`); result.push(`+++ b/${filename}`); // Use a simple LCS-based diff algorithm const diff = simpleDiff(oldLines, newLines); result.push(...diff); return result.join('\n'); } /** * Simple line-by-line diff (not as sophisticated as real git diff, but readable) */ function simpleDiff(oldLines, newLines) { const result = []; const maxLines = Math.max(oldLines.length, newLines.length); let i = 0, j = 0; let hunkStart = -1; let hunkLines = []; const flushHunk = () => { if (hunkLines.length > 0) { result.push(`@@ -${hunkStart + 1} +${hunkStart + 1} @@`); result.push(...hunkLines); hunkLines = []; } }; while (i < oldLines.length || j < newLines.length) { if (i >= oldLines.length) { // Remaining new lines if (hunkStart === -1) hunkStart = i; hunkLines.push(`+${newLines[j]}`); j++; } else if (j >= newLines.length) { // Remaining old lines (removed) if (hunkStart === -1) hunkStart = i; hunkLines.push(`-${oldLines[i]}`); i++; } else if (oldLines[i] === newLines[j]) { // Lines match - context or end of hunk if (hunkLines.length > 0) { // Add some context then close hunk hunkLines.push(` ${oldLines[i]}`); if (hunkLines.length > 20) { flushHunk(); hunkStart = -1; } } i++; j++; } else { // Lines differ if (hunkStart === -1) hunkStart = i; // Look ahead to find if old line appears later in new let foundInNew = false; for (let k = j + 1; k < Math.min(j + 5, newLines.length); k++) { if (oldLines[i] === newLines[k]) { // Old line found later - these are additions while (j < k) { hunkLines.push(`+${newLines[j]}`); j++; } foundInNew = true; break; } } if (!foundInNew) { // Check if new line appears later in old let foundInOld = false; for (let k = i + 1; k < Math.min(i + 5, oldLines.length); k++) { if (newLines[j] === oldLines[k]) { // New line found later - these are deletions while (i < k) { hunkLines.push(`-${oldLines[i]}`); i++; } foundInOld = true; break; } } if (!foundInOld) { // Simple replacement hunkLines.push(`-${oldLines[i]}`); hunkLines.push(`+${newLines[j]}`); i++; j++; } } } // Limit hunk size if (hunkLines.length > 100) { flushHunk(); hunkStart = -1; } } flushHunk(); return result; } /** * Format bytes to human-readable string */ function formatBytes(bytes) { if (bytes === 0) return '0 B'; if (bytes < 0) return `-${formatBytes(Math.abs(bytes))}`; const k = 1024; const sizes = ['B', 'KB', 'MB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`; }