automagik-genie
Version:
Self-evolving AI agent orchestration framework with Model Context Protocol support
224 lines (223 loc) • 9.64 kB
JavaScript
;
/**
* Knowledge diff generation for v2.5.14+ efficient update mechanism
*
* Instead of full backups, generates a diff file showing changes in:
* - AGENTS.md, CLAUDE.md (root files)
* - .genie/ folder contents
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateKnowledgeDiff = generateKnowledgeDiff;
const path_1 = __importDefault(require("path"));
const fs_1 = require("fs");
const fs_utils_1 = require("./fs-utils");
/**
* Generate knowledge diff between old and new Genie installations
*
* @param workspacePath - Root of workspace
* @param oldGeniePath - Path to old .genie folder (before upgrade)
* @param newGeniePath - Path to new .genie folder (after upgrade)
* @param oldVersion - Old version string
* @param newVersion - New version string
* @returns Path to generated diff file
*/
async function generateKnowledgeDiff(workspacePath, oldGeniePath, newGeniePath, oldVersion, newVersion) {
const diffId = (0, fs_utils_1.toIsoId)();
const diffFileName = `update-diff-${oldVersion}-to-${newVersion}-${diffId}.md`;
const diffPath = path_1.default.join(workspacePath, '.genie', 'reports', diffFileName);
await (0, fs_utils_1.ensureDir)(path_1.default.dirname(diffPath));
const oldFiles = await collectKnowledgeFiles(oldGeniePath, workspacePath);
const newFiles = await collectKnowledgeFiles(newGeniePath, workspacePath);
const diffs = await compareFiles(oldFiles, newFiles, oldGeniePath, newGeniePath, workspacePath);
const report = await buildDiffReport(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
};
return { diffPath, diffId, summary };
}
/**
* Collect knowledge files (agents, spells, workflows, product, root docs)
* Excludes user content (wishes, reports, state, backups)
*/
async function collectKnowledgeFiles(geniePath, workspacePath) {
const files = new Set();
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.add(doc);
}
}
if (await (0, fs_utils_1.pathExists)(geniePath)) {
const knowledgeDirs = ['agents', 'spells', 'workflows', 'product', 'code', 'create', 'neurons'];
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) => {
const firstSeg = relPath.split(path_1.default.sep)[0];
const excludeDirs = ['wishes', 'reports', 'state', 'backups', 'scripts'];
return !excludeDirs.includes(firstSeg);
}
});
for (const file of dirFiles) {
files.add(path_1.default.join('.genie', dir, file));
}
}
}
}
return files;
}
/**
* Compare files between old and new installations
*/
async function compareFiles(oldFiles, newFiles, oldGeniePath, newGeniePath, workspacePath) {
const diffs = [];
const allPaths = new Set([...oldFiles, ...newFiles]);
for (const relPath of allPaths) {
const inOld = oldFiles.has(relPath);
const inNew = newFiles.has(relPath);
if (!inOld && inNew) {
const newPath = relPath.startsWith('.genie')
? path_1.default.join(newGeniePath, relPath.replace('.genie/', '').replace('.genie\\', ''))
: path_1.default.join(workspacePath, relPath);
const stats = await fs_1.promises.stat(newPath);
diffs.push({
path: relPath,
status: 'added',
newSize: stats.size
});
}
else if (inOld && !inNew) {
const oldPath = relPath.startsWith('.genie')
? path_1.default.join(oldGeniePath, relPath.replace('.genie/', '').replace('.genie\\', ''))
: path_1.default.join(workspacePath, relPath);
const stats = await fs_1.promises.stat(oldPath);
diffs.push({
path: relPath,
status: 'removed',
oldSize: stats.size
});
}
else {
const oldPath = relPath.startsWith('.genie')
? path_1.default.join(oldGeniePath, relPath.replace('.genie/', '').replace('.genie\\', ''))
: path_1.default.join(workspacePath, relPath);
const newPath = relPath.startsWith('.genie')
? path_1.default.join(newGeniePath, relPath.replace('.genie/', '').replace('.genie\\', ''))
: path_1.default.join(workspacePath, relPath);
const [oldContent, newContent] = await Promise.all([
fs_1.promises.readFile(oldPath, 'utf8'),
fs_1.promises.readFile(newPath, 'utf8')
]);
const [oldStats, newStats] = await Promise.all([
fs_1.promises.stat(oldPath),
fs_1.promises.stat(newPath)
]);
if (oldContent !== newContent) {
diffs.push({
path: relPath,
status: 'modified',
oldSize: oldStats.size,
newSize: newStats.size
});
}
else {
diffs.push({
path: relPath,
status: 'unchanged',
oldSize: oldStats.size,
newSize: newStats.size
});
}
}
}
return diffs.sort((a, b) => a.path.localeCompare(b.path));
}
/**
* Build markdown diff report
*/
async function buildDiffReport(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 Knowledge Update Diff`);
lines.push(``);
lines.push(`**Version Transition:** ${oldVersion} → ${newVersion}`);
lines.push(`**Diff ID:** ${diffId}`);
lines.push(`**Generated:** ${new Date().toISOString()}`);
lines.push(``);
lines.push(`## Summary`);
lines.push(``);
lines.push(`- **Added:** ${added.length} files`);
lines.push(`- **Removed:** ${removed.length} files`);
lines.push(`- **Modified:** ${modified.length} files`);
lines.push(`- **Total Changes:** ${added.length + removed.length + modified.length} files`);
lines.push(``);
if (added.length > 0) {
lines.push(`## Added Files (${added.length})`);
lines.push(``);
for (const diff of added) {
const size = diff.newSize ? ` (${formatBytes(diff.newSize)})` : '';
lines.push(`- ✅ \`${diff.path}\`${size}`);
}
lines.push(``);
}
if (removed.length > 0) {
lines.push(`## Removed Files (${removed.length})`);
lines.push(``);
for (const diff of removed) {
const size = diff.oldSize ? ` (${formatBytes(diff.oldSize)})` : '';
lines.push(`- ❌ \`${diff.path}\`${size}`);
}
lines.push(``);
}
if (modified.length > 0) {
lines.push(`## Modified Files (${modified.length})`);
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}\` (${oldSize} → ${newSize}${delta})`);
}
lines.push(``);
}
lines.push(`## Notes`);
lines.push(``);
lines.push(`This diff shows changes in Genie's "knowledge" (framework files) between versions.`);
lines.push(`User content (wishes, reports, state) is excluded from this diff.`);
lines.push(``);
lines.push(`**Included in diff:**`);
lines.push(`- Root documentation: AGENTS.md, CLAUDE.md`);
lines.push(`- Framework directories: .genie/agents/, .genie/spells/, .genie/workflows/, .genie/product/`);
lines.push(`- Collective directories: .genie/code/, .genie/create/, .genie/neurons/`);
lines.push(``);
lines.push(`**Excluded from diff:**`);
lines.push(`- User content: .genie/wishes/, .genie/reports/`);
lines.push(`- Runtime state: .genie/state/, .genie/backups/`);
lines.push(`- Helper scripts: .genie/scripts/helpers/`);
lines.push(``);
return lines.join('\n');
}
/**
* Format bytes to human-readable string
*/
function formatBytes(bytes) {
if (bytes === 0)
return '0 B';
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]}`;
}