@stackmemoryai/stackmemory
Version:
Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.
495 lines (421 loc) • 16.3 kB
JavaScript
/**
* GEPA Auto-Optimizer
*
* Watches CLAUDE.md for changes and automatically runs optimization.
* Shows before/after comparison with metrics.
*
* Usage:
* node auto-optimize.js watch [path] # Watch and auto-optimize
* node auto-optimize.js compare [a] [b] # Compare two versions
* node auto-optimize.js report # Show optimization report
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { execSync, spawn } from 'child_process';
import crypto from 'crypto';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const GEPA_DIR = path.join(__dirname, '..');
const GENERATIONS_DIR = path.join(GEPA_DIR, 'generations');
const RESULTS_DIR = path.join(GEPA_DIR, 'results');
// ANSI colors
const c = {
reset: '\x1b[0m',
bold: '\x1b[1m',
dim: '\x1b[2m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
bgGreen: '\x1b[42m',
bgRed: '\x1b[41m',
bgYellow: '\x1b[43m',
};
/**
* Calculate content hash for change detection
*/
function hashContent(content) {
return crypto.createHash('md5').update(content).digest('hex').slice(0, 8);
}
/**
* Analyze markdown structure
*/
function analyzeMarkdown(content) {
const lines = content.split('\n');
return {
totalLines: lines.length,
totalChars: content.length,
estimatedTokens: Math.ceil(content.length / 4),
// Structure
h1Count: (content.match(/^# /gm) || []).length,
h2Count: (content.match(/^## /gm) || []).length,
h3Count: (content.match(/^### /gm) || []).length,
// Content types
codeBlocks: (content.match(/```/g) || []).length / 2,
bulletPoints: (content.match(/^[-*] /gm) || []).length,
numberedLists: (content.match(/^\d+\. /gm) || []).length,
// Keywords (instruction density)
mustCount: (content.match(/\bMUST\b/gi) || []).length,
neverCount: (content.match(/\bNEVER\b/gi) || []).length,
alwaysCount: (content.match(/\bALWAYS\b/gi) || []).length,
importantCount: (content.match(/\bIMPORTANT\b/gi) || []).length,
// Sections
sections: [...content.matchAll(/^##+ (.+)$/gm)].map((m) => m[1]),
};
}
/**
* Format comparison table
*/
function formatComparison(before, after, label = 'Metric') {
const metrics = [
['Lines', before.totalLines, after.totalLines],
['Characters', before.totalChars, after.totalChars],
['Est. Tokens', before.estimatedTokens, after.estimatedTokens],
['H2 Sections', before.h2Count, after.h2Count],
['Code Blocks', before.codeBlocks, after.codeBlocks],
['Bullet Points', before.bulletPoints, after.bulletPoints],
['MUST rules', before.mustCount, after.mustCount],
['NEVER rules', before.neverCount, after.neverCount],
['ALWAYS rules', before.alwaysCount, after.alwaysCount],
];
console.log(
`\n${c.bold}${c.cyan}╔════════════════════════════════════════════════════════════╗${c.reset}`
);
console.log(
`${c.bold}${c.cyan}║${c.reset} ${c.bold}BEFORE / AFTER COMPARISON${c.reset} ${c.cyan}║${c.reset}`
);
console.log(
`${c.cyan}╠════════════════════════════════════════════════════════════╣${c.reset}`
);
console.log(
`${c.cyan}║${c.reset} ${c.dim}Metric${c.reset} ${c.dim}Before${c.reset} ${c.dim}After${c.reset} ${c.dim}Change${c.reset} ${c.cyan}║${c.reset}`
);
console.log(
`${c.cyan}╠════════════════════════════════════════════════════════════╣${c.reset}`
);
metrics.forEach(([name, b, a]) => {
const diff = a - b;
const pct = b > 0 ? ((diff / b) * 100).toFixed(0) : '∞';
const sign = diff > 0 ? '+' : '';
const color = diff > 0 ? c.green : diff < 0 ? c.red : c.dim;
const nameStr = name.padEnd(18);
const beforeStr = String(b).padStart(8);
const afterStr = String(a).padStart(8);
const changeStr = `${sign}${diff} (${sign}${pct}%)`.padStart(12);
console.log(
`${c.cyan}║${c.reset} ${nameStr} ${beforeStr} ${c.bold}${afterStr}${c.reset} ${color}${changeStr}${c.reset} ${c.cyan}║${c.reset}`
);
});
console.log(
`${c.cyan}╚════════════════════════════════════════════════════════════╝${c.reset}`
);
}
/**
* Show section diff
*/
function showSectionDiff(before, after) {
const beforeSections = new Set(before.sections);
const afterSections = new Set(after.sections);
const added = after.sections.filter((s) => !beforeSections.has(s));
const removed = before.sections.filter((s) => !afterSections.has(s));
const kept = before.sections.filter((s) => afterSections.has(s));
if (added.length || removed.length) {
console.log(`\n${c.bold}Section Changes:${c.reset}`);
if (removed.length) {
console.log(`${c.red} Removed:${c.reset}`);
removed.forEach((s) => console.log(`${c.red} - ${s}${c.reset}`));
}
if (added.length) {
console.log(`${c.green} Added:${c.reset}`);
added.forEach((s) => console.log(`${c.green} + ${s}${c.reset}`));
}
}
}
/**
* Show inline diff (simplified)
*/
function showInlineDiff(beforeContent, afterContent) {
const beforeLines = beforeContent.split('\n');
const afterLines = afterContent.split('\n');
console.log(`\n${c.bold}Key Changes (first 20 diffs):${c.reset}`);
console.log(c.dim + '─'.repeat(60) + c.reset);
let diffCount = 0;
const maxDiffs = 20;
// Simple line-by-line diff
const maxLen = Math.max(beforeLines.length, afterLines.length);
for (let i = 0; i < maxLen && diffCount < maxDiffs; i++) {
const b = beforeLines[i] || '';
const a = afterLines[i] || '';
if (b !== a) {
if (b && !a) {
console.log(
`${c.red}- L${i + 1}: ${b.slice(0, 70)}${b.length > 70 ? '...' : ''}${c.reset}`
);
} else if (!b && a) {
console.log(
`${c.green}+ L${i + 1}: ${a.slice(0, 70)}${a.length > 70 ? '...' : ''}${c.reset}`
);
} else if (b.trim() !== a.trim()) {
console.log(`${c.yellow}~ L${i + 1}:${c.reset}`);
console.log(
`${c.red} - ${b.slice(0, 60)}${b.length > 60 ? '...' : ''}${c.reset}`
);
console.log(
`${c.green} + ${a.slice(0, 60)}${a.length > 60 ? '...' : ''}${c.reset}`
);
}
diffCount++;
}
}
if (diffCount >= maxDiffs) {
console.log(`${c.dim} ... and more changes${c.reset}`);
}
}
/**
* Compare two versions
*/
function compare(pathA, pathB) {
const contentA = fs.readFileSync(pathA, 'utf8');
const contentB = fs.readFileSync(pathB, 'utf8');
const analysisA = analyzeMarkdown(contentA);
const analysisB = analyzeMarkdown(contentB);
console.log(`\n${c.bold}${c.magenta}GEPA Comparison Report${c.reset}`);
console.log(`${c.dim}Before: ${pathA}${c.reset}`);
console.log(`${c.dim}After: ${pathB}${c.reset}`);
formatComparison(analysisA, analysisB);
showSectionDiff(analysisA, analysisB);
showInlineDiff(contentA, contentB);
// Score summary
const tokenChange = analysisB.estimatedTokens - analysisA.estimatedTokens;
const ruleChange =
analysisB.mustCount +
analysisB.neverCount +
analysisB.alwaysCount -
(analysisA.mustCount + analysisA.neverCount + analysisA.alwaysCount);
console.log(`\n${c.bold}Summary:${c.reset}`);
console.log(
` Token budget: ${tokenChange >= 0 ? c.yellow + '+' : c.green}${tokenChange}${c.reset} tokens`
);
console.log(
` Rule density: ${ruleChange >= 0 ? c.green + '+' : c.red}${ruleChange}${c.reset} explicit rules`
);
}
/**
* Watch for changes and auto-optimize
*/
async function watch(targetPath) {
const claudeMdPath = targetPath || path.join(process.cwd(), 'CLAUDE.md');
if (!fs.existsSync(claudeMdPath)) {
console.error(`${c.red}Error: ${claudeMdPath} not found${c.reset}`);
process.exit(1);
}
let lastHash = hashContent(fs.readFileSync(claudeMdPath, 'utf8'));
let isOptimizing = false;
let optimizeQueue = false;
console.log(`${c.bold}${c.cyan}GEPA Auto-Optimizer${c.reset}`);
console.log(`${c.dim}Watching: ${claudeMdPath}${c.reset}`);
console.log(`${c.dim}Press Ctrl+C to stop${c.reset}\n`);
// Initial analysis
const initial = analyzeMarkdown(fs.readFileSync(claudeMdPath, 'utf8'));
console.log(`${c.bold}Current state:${c.reset}`);
console.log(
` ${initial.totalLines} lines, ~${initial.estimatedTokens} tokens`
);
console.log(
` ${initial.h2Count} sections, ${initial.mustCount + initial.neverCount + initial.alwaysCount} explicit rules\n`
);
// Watch loop
const checkInterval = setInterval(async () => {
try {
const content = fs.readFileSync(claudeMdPath, 'utf8');
const currentHash = hashContent(content);
if (currentHash !== lastHash) {
console.log(
`\n${c.yellow}⚡ Change detected!${c.reset} (${new Date().toLocaleTimeString()})`
);
// Save before state
const beforePath = path.join(GEPA_DIR, '.before-optimize.md');
const statePath = path.join(GEPA_DIR, 'state.json');
if (fs.existsSync(statePath)) {
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
const currentBest = path.join(
GENERATIONS_DIR,
`gen-${String(state.currentGeneration).padStart(3, '0')}`,
`${state.bestVariant}.md`
);
if (fs.existsSync(currentBest)) {
fs.copyFileSync(currentBest, beforePath);
}
}
lastHash = currentHash;
if (isOptimizing) {
optimizeQueue = true;
console.log(
`${c.dim} Optimization in progress, queued for next run${c.reset}`
);
return;
}
isOptimizing = true;
// Run quick optimization (1 generation)
console.log(`${c.cyan} Running GEPA optimization...${c.reset}`);
try {
// Re-init with new content
execSync(
`node ${path.join(GEPA_DIR, 'optimize.js')} init ${claudeMdPath}`,
{
stdio: 'pipe',
}
);
// Quick mutate + score
execSync(`node ${path.join(GEPA_DIR, 'optimize.js')} mutate`, {
stdio: 'pipe',
});
execSync(`node ${path.join(GEPA_DIR, 'optimize.js')} score`, {
stdio: 'pipe',
});
// Show comparison
const afterPath = path.join(GENERATIONS_DIR, 'current');
if (fs.existsSync(beforePath) && fs.existsSync(afterPath)) {
const resolvedAfter = fs.realpathSync(afterPath);
compare(beforePath, resolvedAfter);
}
// Load state for summary
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
console.log(`\n${c.green}✓ Optimization complete${c.reset}`);
console.log(
` Best variant: ${c.bold}${state.bestVariant}${c.reset}`
);
console.log(
` Score: ${c.bold}${(state.bestScore * 100).toFixed(1)}%${c.reset}`
);
console.log(
`\n${c.dim} To apply: cp ${path.join(GENERATIONS_DIR, 'current')} ${claudeMdPath}${c.reset}`
);
} catch (e) {
console.error(
`${c.red} Optimization failed: ${e.message}${c.reset}`
);
}
isOptimizing = false;
// Process queue
if (optimizeQueue) {
optimizeQueue = false;
lastHash = ''; // Force re-check
}
}
} catch (e) {
// File might be temporarily unavailable during write
}
}, 2000); // Check every 2 seconds
// Handle shutdown
process.on('SIGINT', () => {
clearInterval(checkInterval);
console.log(`\n${c.dim}Watcher stopped${c.reset}`);
process.exit(0);
});
}
/**
* Generate optimization report
*/
function report() {
const statePath = path.join(GEPA_DIR, 'state.json');
if (!fs.existsSync(statePath)) {
console.error(`${c.red}No GEPA state found. Run 'init' first.${c.reset}`);
return;
}
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
console.log(
`\n${c.bold}${c.magenta}═══════════════════════════════════════════════════════════${c.reset}`
);
console.log(
`${c.bold}${c.magenta} GEPA OPTIMIZATION REPORT ${c.reset}`
);
console.log(
`${c.bold}${c.magenta}═══════════════════════════════════════════════════════════${c.reset}\n`
);
console.log(`${c.bold}Current State:${c.reset}`);
console.log(` Generation: ${c.cyan}${state.currentGeneration}${c.reset}`);
console.log(` Best Variant: ${c.green}${state.bestVariant}${c.reset}`);
console.log(
` Best Score: ${c.bold}${(state.bestScore * 100).toFixed(1)}%${c.reset}`
);
console.log(` Target File: ${c.dim}${state.targetPath}${c.reset}`);
// History summary
if (state.history && state.history.length > 0) {
console.log(`\n${c.bold}Evolution History:${c.reset}`);
const scoreHistory = state.history
.filter((h) => h.action === 'select' && h.scores)
.map((h) => ({
gen: h.generation,
best: h.best,
score: h.scores.find((s) => s.variant === h.best)?.score || 0,
}));
if (scoreHistory.length > 0) {
// ASCII chart
const maxScore = Math.max(...scoreHistory.map((h) => h.score));
const chartWidth = 40;
scoreHistory.forEach((h) => {
const barLen = Math.round((h.score / maxScore) * chartWidth);
const bar = '█'.repeat(barLen) + '░'.repeat(chartWidth - barLen);
const pct = (h.score * 100).toFixed(0).padStart(3);
console.log(
` Gen ${String(h.gen).padStart(2)}: ${c.green}${bar}${c.reset} ${pct}% (${h.best})`
);
});
// Improvement
if (scoreHistory.length > 1) {
const first = scoreHistory[0].score;
const last = scoreHistory[scoreHistory.length - 1].score;
const improvement = (((last - first) / first) * 100).toFixed(1);
console.log(
`\n ${c.bold}Total improvement: ${improvement >= 0 ? c.green + '+' : c.red}${improvement}%${c.reset}`
);
}
}
}
// Show before/after if available
const beforePath = path.join(GEPA_DIR, '.before-optimize.md');
const currentPath = path.join(GENERATIONS_DIR, 'current');
if (fs.existsSync(beforePath) && fs.existsSync(currentPath)) {
console.log(`\n${c.bold}Latest Optimization:${c.reset}`);
compare(beforePath, fs.realpathSync(currentPath));
}
console.log(
`\n${c.dim}Run 'node auto-optimize.js watch' to auto-optimize on changes${c.reset}\n`
);
}
// CLI
const command = process.argv[2];
const arg1 = process.argv[3];
const arg2 = process.argv[4];
switch (command) {
case 'watch':
watch(arg1);
break;
case 'compare':
if (!arg1 || !arg2) {
console.error('Usage: compare <before.md> <after.md>');
process.exit(1);
}
compare(arg1, arg2);
break;
case 'report':
report();
break;
default:
console.log(`
${c.bold}GEPA Auto-Optimizer${c.reset}
Usage:
node auto-optimize.js watch [path] Watch CLAUDE.md and auto-optimize
node auto-optimize.js compare <a> <b> Compare two versions
node auto-optimize.js report Show optimization report
Examples:
node auto-optimize.js watch ./CLAUDE.md
node auto-optimize.js compare gen-000/baseline.md gen-001/variant-a.md
node auto-optimize.js report
`);
}
export { compare, analyzeMarkdown, watch, report };