fix(dashboard): 3 UI bugs + new DB watch tool

1. filterCategory: fix inline event.target → uses btn parameter
   - All Agents tab filter buttons now correctly toggle active class

2. exportRecommendations/showApplyModal: read from agentData, not removed INLINE_RECOMMENDATIONS
   - Apply modal shows real recommendations
   - Export generates JSON with real data

3. Heatmap cell click: add showCellDetail modal with Chart.js line chart + prompt history
   - onclick='showCellDetail(model, agent)' on every td
   - renderCellChart computes score history from agent.history
   - prompt_change items filtered and displayed

4. watch-db.cjs: incremental DB sync tool
   - Polls git for changes in .kilo/agents/*.md and kilo-meta.json
   - Detects model_change vs prompt_change by comparing with previous version
   - Exports to JSON after sync, logs to .kilo/logs/watch-db.log
   - SIGINT/SIGTERM graceful shutdown
   - Trigger: npm run evolution:watch
This commit is contained in:
Deploy Bot
2026-05-25 21:50:55 +01:00
parent a0604afaf6
commit 7f1269a370
5 changed files with 773 additions and 24 deletions

View File

@@ -30,6 +30,19 @@ The `build-standalone-fixed.cjs` script:
4. Embeds unified JSON data directly into the HTML file
5. Updates JavaScript functions to use embedded data
## Incremental DB Sync
The `watch-db.cjs` script provides incremental database synchronization:
1. Watches for changes in `.kilo/agents/*.md` and `kilo-meta.json`
2. Only processes changed files (incremental update)
3. Determines change type (model_change vs prompt_change)
4. Updates database with new versions and metadata
5. Exports updated data to JSON
6. Clean shutdown on SIGINT/SIGTERM
7. Configurable polling interval via `WATCH_INTERVAL_MS` env var
8. Logging to `.kilo/logs/watch-db.log`
## Validation
The build process ensures:
@@ -47,6 +60,18 @@ The build process ensures:
Simply open `index.standalone.html` in any modern browser. No server or external dependencies required.
To run the incremental DB watcher:
```bash
# Run with default 60 second interval
node agent-evolution/scripts/watch-db.cjs
# Run with custom interval (10 seconds)
WATCH_INTERVAL_MS=10000 node agent-evolution/scripts/watch-db.cjs
# Run in background
nohup node agent-evolution/scripts/watch-db.cjs > watch-db.log 2>&1 &
```
## Agent Count
The dashboard currently tracks **34 agents** across multiple categories:

View File

@@ -794,13 +794,13 @@
<input type="text" class="search-input" id="agentSearch" placeholder="Search agents..." oninput="filterAgents()">
</div>
<div class="filter-row">
<button class="filter-btn active" onclick="filterCategory('all')">All</button>
<button class="filter-btn" onclick="filterCategory('Core Dev')">Core Dev</button>
<button class="filter-btn" onclick="filterCategory('QA')">QA</button>
<button class="filter-btn" onclick="filterCategory('Security')">Security</button>
<button class="filter-btn" onclick="filterCategory('Analysis')">Analysis</button>
<button class="filter-btn" onclick="filterCategory('Process')">Process</button>
<button class="filter-btn" onclick="filterCategory('Cognitive')">Cognitive</button>
<button class="filter-btn active" onclick="filterCategory('all', this)">All</button>
<button class="filter-btn" onclick="filterCategory('Core Dev', this)">Core Dev</button>
<button class="filter-btn" onclick="filterCategory('QA', this)">QA</button>
<button class="filter-btn" onclick="filterCategory('Security', this)">Security</button>
<button class="filter-btn" onclick="filterCategory('Analysis', this)">Analysis</button>
<button class="filter-btn" onclick="filterCategory('Process', this)">Process</button>
<button class="filter-btn" onclick="filterCategory('Cognitive', this)">Cognitive</button>
</div>
<div id="agentsByCategory"></div>
</div>
@@ -990,6 +990,21 @@
</div>
</div>
<!-- Cell Detail Modal -->
<div id="cellDetailModal" class="modal">
<div class="modal-content" style="max-width:800px">
<div class="modal-header">
<div class="modal-title">Agent Model Performance</div>
<div class="modal-actions">
<button class="action-btn" onclick="closeCellDetailModal()"></button>
</div>
</div>
<div class="modal-body">
<div id="cellDetailContent"></div>
</div>
</div>
</div>
<script>
// Agent Evolution Dashboard
// Supports both server and file:// mode
@@ -1475,10 +1490,10 @@ function renderHeatmap() {
let marks = '';
if (best) marks += '<span class="hm-star">★</span>';
if (ifLow) marks += '<span class="hm-if-warn">⚠</span>';
h += `<td style="background:${hmColor(s)};color:${hmText(s)}" class="${cur ? 'hm-cur' : ''}" title="${ag.n} × ${hmModels[j].n}: ${s}"
h += `<td style="background:${hmColor(s)};color:${hmText(s)};cursor:pointer" class="${cur ? 'hm-cur' : ''}" title="${ag.n} × ${hmModels[j].n}: ${s}"
onmouseover="showTT(event,'${ag.n}','${hmModels[j].n} (${hmModels[j].p})',${s},${best},${cur},${hmModels[j].if})"
onmouseout="hideTT()"
onclick="openHmModal(event,'${ag.n}','${hmModels[j].n}',${s},${hmModels[j].if})">${s}${marks}</td>`;
onclick="showCellDetail('${hmModels[j].full}', '${ag.n}')">${s}${marks}</td>`;
});
h += '</tr>';
});
@@ -1540,6 +1555,163 @@ function closeHmModal() {
document.getElementById('hmModal').style.display = 'none';
}
// Show cell detail modal with Chart.js line chart and prompt history
function showCellDetail(modelName, agentName) {
const agent = agentData.agents[agentName];
if (!agent) {
console.error('Agent not found:', agentName);
return;
}
// Set modal title
document.querySelector('#cellDetailModal .modal-title').textContent = `${agentName} × ${modelName.split('/').pop()}`;
// Generate content
let content = `
<div style="margin-bottom: 20px;">
<h3 style="margin-bottom: 10px;">Performance Over Time</h3>
<div style="position: relative; height: 300px;">
<canvas id="cellChartCanvas"></canvas>
</div>
</div>
<div>
<h3 style="margin-bottom: 10px;">Prompt Change History</h3>
<div id="promptHistoryList" style="max-height: 300px; overflow-y: auto;">
`;
// Filter prompt changes from history
const promptChanges = (agent.history || []).filter(item => item.change_type === 'prompt_change');
if (promptChanges.length > 0) {
content += '<ul style="list-style: none; padding: 0;">';
promptChanges.forEach(change => {
content += `
<li style="padding: 10px; border-bottom: 1px solid var(--border); margin-bottom: 10px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
<span style="font-family: 'JetBrains Mono', monospace; font-size: 0.8em; color: var(--text-muted);">
${formatDate(change.date)}
</span>
<span style="font-family: 'JetBrains Mono', monospace; font-size: 0.8em; color: var(--accent-cyan);">
${change.commit ? change.commit.substring(0, 7) : 'unknown'}
</span>
</div>
<div style="font-size: 0.9em; color: var(--text-secondary);">${change.reason || 'No reason provided'}</div>
</li>
`;
});
content += '</ul>';
} else {
content += '<p style="color: var(--text-muted); text-align: center; padding: 20px;">No prompt change history found</p>';
}
content += '</div></div>';
// Set content
document.getElementById('cellDetailContent').innerHTML = content;
// Render chart
renderCellChart(agentName, modelName);
// Show modal
document.getElementById('cellDetailModal').classList.add('show');
}
// Render Chart.js line chart for agent performance over time
function renderCellChart(agentName, modelName) {
const ctx = document.getElementById('cellChartCanvas')?.getContext('2d');
if (!ctx) return;
// Get agent data
const agent = agentData.agents[agentName];
if (!agent) return;
// Generate data points from history
const labels = [];
const scores = [];
// Add initial point
if (agent.history && agent.history.length > 0) {
const first = agent.history[0];
labels.push(formatDate(first.date));
scores.push(computeAgentScore(first.from || modelName));
}
// Add points from history
(agent.history || []).forEach(item => {
labels.push(formatDate(item.date));
scores.push(computeAgentScore(item.to || modelName));
});
// Create chart
new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Agent Performance Score',
data: scores,
borderColor: '#00d4ff',
backgroundColor: 'rgba(0, 212, 255, 0.1)',
borderWidth: 2,
pointBackgroundColor: '#00ff94',
pointRadius: 4,
pointHoverRadius: 6,
fill: true,
tension: 0.3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: '#0f1525',
titleColor: '#e8f1ff',
bodyColor: '#8ba3c0',
borderColor: '#1e2d45',
borderWidth: 1
}
},
scales: {
x: {
grid: {
color: '#1e2d45'
},
ticks: {
color: '#5a7090',
font: {
family: 'JetBrains Mono',
size: 10
}
}
},
y: {
grid: {
color: '#1e2d45'
},
ticks: {
color: '#5a7090',
font: {
family: 'JetBrains Mono',
size: 10
}
},
min: 0,
max: 100
}
}
}
});
}
// Close cell detail modal
function closeCellDetailModal() {
document.getElementById('cellDetailModal').classList.remove('show');
}
// Close modal when clicking outside
document.addEventListener('click', function(e) {
const hmModal = document.getElementById('hmModal');
@@ -1558,6 +1730,12 @@ document.addEventListener('click', function(e) {
if (researchModal.classList.contains('show') && !e.target.closest('.modal-content')) {
closeResearchModal();
}
// Close cell detail modal when clicking outside
const cellDetailModal = document.getElementById('cellDetailModal');
if (cellDetailModal.classList.contains('show') && !e.target.closest('.modal-content')) {
closeCellDetailModal();
}
});
function switchHmTab(tabName) {
@@ -2009,9 +2187,9 @@ function filterAgents() {
});
}
function filterCategory(category) {
function filterCategory(category, btn) {
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
btn.classList.add('active');
if (category === 'all') {
document.querySelectorAll('.agent-card').forEach(card => card.style.display = '');
@@ -2025,15 +2203,23 @@ function filterCategory(category) {
// Export
function exportRecommendations() {
let recs = INLINE_RECOMMENDATIONS && INLINE_RECOMMENDATIONS.length > 0
? INLINE_RECOMMENDATIONS
: Object.entries(agentData.agents)
.filter(([_, a]) => a.current.recommendations && a.current.recommendations.length > 0)
.map(([name, agent]) => ({
agent: name,
current_model: agent.current.model,
recommendations: agent.current.recommendations
}));
let recs = [];
Object.entries(agentData.agents).forEach(([name, agent]) => {
if (agent.current.recommendations && agent.current.recommendations.length > 0) {
agent.current.recommendations.forEach(rec => {
recs.push({
agent: name,
current_model: agent.current.model,
recommended_model: rec.target,
impact: rec.priority || 'medium',
score_before: rec.score_before || 0,
score_after: rec.score_after || 0,
score_delta: rec.score_delta || 0,
rationale: rec.reason || ''
});
});
}
});
const output = {
timestamp: new Date().toISOString(),
@@ -2068,12 +2254,29 @@ function closeModal() {
// Apply Fixes Modal
function showApplyModal() {
const recs = INLINE_RECOMMENDATIONS && INLINE_RECOMMENDATIONS.length > 0 ? INLINE_RECOMMENDATIONS : [];
const recs = [];
Object.entries(agentData.agents).forEach(([name, agent]) => {
if (agent.current.recommendations && agent.current.recommendations.length > 0) {
agent.current.recommendations.forEach(rec => {
recs.push({
agent: name,
current_model: agent.current.model,
recommended_model: rec.target,
impact: rec.priority || 'medium',
score_before: rec.score_before || 0,
score_after: rec.score_after || 0,
score_delta: rec.score_delta || 0,
rationale: rec.reason || ''
});
});
}
});
const checklist = document.getElementById('applyChecklist');
checklist.innerHTML = recs.map((r, idx) => {
const fromModel = r.current_model_in_agent_versions || r.current_model || '';
const toModel = r.source_of_truth_model || r.recommended_model || '';
const fromModel = r.current_model || '';
const toModel = r.recommended_model || '';
const fromShort = fromModel.split('/').pop() || fromModel;
const toShort = toModel.split('/').pop() || toModel;
const impact = (r.impact || 'low').toLowerCase();

View File

@@ -194,7 +194,7 @@ function renderHeatmap() {
h += '<td style="background:' + hmColor(s) + ';color:' + hmText(s) + '" class="' + (cur ? 'hm-cur' : '') + '" title="' + ag.n + ' × ' + hmModels[j].n + ': ' + s + '"' +
' onmouseover="showTT(event,\\\'' + ag.n + '\\\',\\\'' + hmModels[j].n + ' (' + hmModels[j].p + ')\\\',' + s + ',' + best + ',' + cur + ',' + hmModels[j].if + ')"' +
' onmouseout="hideTT()"' +
' onclick="openHmModal(event,\\\'' + ag.n + '\\\',\\\'' + hmModels[j].n + '\\\',' + s + ',' + hmModels[j].if + ')">' + s + marks + '</td>';
' onclick="showCellDetail(\'' + hmModels[j].full + '\', \'' + ag.n + '\')">' + s + marks + '</td>';
});
h += '</tr>';
});

View File

@@ -0,0 +1,520 @@
#!/usr/bin/env node
/**
* Watch for changes in agent files and update database incrementally
*
* Features:
* 1. Poll git for changes in .kilo/agents/*.md and kilo-meta.json
* 2. On change, only process changed files (incremental update)
* 3. Determine change type (model_change vs prompt_change)
* 4. Update database with new versions and metadata
* 5. Export updated data to JSON
* 6. Clean shutdown on SIGINT/SIGTERM
* 7. Configurable polling interval via WATCH_INTERVAL_MS
* 8. Logging to .kilo/logs/watch-db.log
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Try to load sqlite3, but make it optional
let sqlite3;
try {
sqlite3 = require('sqlite3').verbose();
} catch (e) {
console.warn('sqlite3 not available, database functionality will be limited');
sqlite3 = null;
}
// Configuration
const DB_PATH = path.join(__dirname, '../data/agent-evolutions.db');
const LOG_FILE = path.join(__dirname, '../../.kilo/logs/watch-db.log');
const WATCH_INTERVAL_MS = parseInt(process.env.WATCH_INTERVAL_MS || '60000'); // Default 60 seconds
// Ensure log directory exists
const logDir = path.dirname(LOG_FILE);
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
// Logging function
function log(level, message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${level.toUpperCase()}: ${message}\n`;
console.log(logMessage.trim());
fs.appendFileSync(LOG_FILE, logMessage);
}
// Get current HEAD commit for tracked files
function getCurrentCommit() {
try {
const output = execSync(
'git log -1 --format="%H" -- .kilo/agents/*.md kilo-meta.json',
{ cwd: path.join(__dirname, '../..'), encoding: 'utf-8' }
).trim();
return output || null;
} catch (error) {
log('error', `Failed to get current commit: ${error.message}`);
return null;
}
}
// Get last synced commit from database
function getLastSyncedCommit(db) {
// If sqlite3 is not available, return null to trigger first run
if (!sqlite3) {
return Promise.resolve(null);
}
return new Promise((resolve, reject) => {
db.get("SELECT value FROM meta WHERE key = 'last_synced_commit'", (err, row) => {
if (err) {
reject(err);
} else {
resolve(row ? row.value : null);
}
});
});
}
// Update last synced commit in database
function updateLastSyncedCommit(db, commit) {
// If sqlite3 is not available, just resolve
if (!sqlite3) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
db.run(
"INSERT OR REPLACE INTO meta (key, value) VALUES ('last_synced_commit', ?)",
[commit],
(err) => {
if (err) {
reject(err);
} else {
resolve();
}
}
);
});
}
// Get changed files between commits
function getChangedFiles(sinceCommit) {
try {
const command = sinceCommit
? `git log --name-only --oneline ${sinceCommit}..HEAD -- .kilo/agents/*.md kilo-meta.json`
: 'git log -1 --name-only --oneline HEAD -- .kilo/agents/*.md kilo-meta.json';
const output = execSync(command, {
cwd: path.join(__dirname, '../..'),
encoding: 'utf-8'
});
// Parse output to get file paths
const lines = output.trim().split('\n');
const files = [];
for (const line of lines) {
if (line && (line.endsWith('.md') || line.endsWith('kilo-meta.json'))) {
files.push(line.trim());
}
}
return [...new Set(files)]; // Remove duplicates
} catch (error) {
log('error', `Failed to get changed files: ${error.message}`);
return [];
}
}
// Parse YAML frontmatter from file
function parseYamlFrontmatter(content) {
if (!content.startsWith('---')) return null;
const end = content.indexOf('---', 4);
if (end === -1) return null;
const frontmatter = content.slice(4, end).trim();
const lines = frontmatter.split('\n');
const result = {};
for (const line of lines) {
const match = line.match(/^([a-zA-Z_]+):\s*(.*)$/);
if (match) {
const key = match[1];
let value = match[2].trim();
// Remove quotes if present
if (value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1);
} else if (value.startsWith("'") && value.endsWith("'")) {
value = value.slice(1, -1);
}
result[key] = value;
}
}
return result;
}
// Get previous version of an agent from database
function getPreviousAgentVersion(db, agentName) {
// If sqlite3 is not available, return null
if (!sqlite3) {
return Promise.resolve(null);
}
return new Promise((resolve, reject) => {
db.get(
"SELECT model FROM agent_versions WHERE agent_name = ? ORDER BY date DESC LIMIT 1",
[agentName],
(err, row) => {
if (err) {
reject(err);
} else {
resolve(row ? row.model : null);
}
}
);
});
}
// Insert new agent version into database
function insertAgentVersion(db, agentData) {
// If sqlite3 is not available, just resolve
if (!sqlite3) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const {
agent_name,
model,
date,
commit,
reason,
change_type
} = agentData;
db.run(
"INSERT INTO agent_versions (agent_name, model, date, commit, reason, change_type) VALUES (?, ?, ?, ?, ?, ?)",
[agent_name, model, date, commit, reason, change_type],
(err) => {
if (err) {
reject(err);
} else {
resolve();
}
}
);
});
}
// Process changed agent file
async function processAgentFile(db, filePath, commitInfo) {
try {
const agentName = path.basename(filePath, '.md');
const fullPath = path.join(__dirname, '../../', filePath);
// Read file content
const content = fs.readFileSync(fullPath, 'utf-8');
const frontmatter = parseYamlFrontmatter(content);
if (!frontmatter) {
log('warn', `Could not parse frontmatter for ${filePath}`);
return;
}
// Get previous version
const previousModel = await getPreviousAgentVersion(db, agentName);
// Determine change type
let changeType = 'prompt_change';
if (previousModel && frontmatter.model !== previousModel) {
changeType = 'model_change';
}
// Insert new version
await insertAgentVersion(db, {
agent_name: agentName,
model: frontmatter.model || 'unknown',
date: commitInfo.date,
commit: commitInfo.hash,
reason: commitInfo.message,
change_type: changeType
});
log('info', `Processed ${agentName}: ${changeType}`);
} catch (error) {
log('error', `Failed to process ${filePath}: ${error.message}`);
}
}
// Process kilo-meta.json changes
async function processKiloMetaChanges(db, commitInfo) {
try {
const metaPath = path.join(__dirname, '../../kilo-meta.json');
const content = fs.readFileSync(metaPath, 'utf-8');
const metaData = JSON.parse(content);
// For each agent in meta, check if model changed
for (const [agentName, agentData] of Object.entries(metaData.agents)) {
// Get previous version
const previousModel = await getPreviousAgentVersion(db, agentName);
// Check if model changed
if (previousModel && agentData.model !== previousModel) {
// Insert model change record
await insertAgentVersion(db, {
agent_name: agentName,
model: agentData.model,
date: commitInfo.date,
commit: commitInfo.hash,
reason: commitInfo.message,
change_type: 'model_change'
});
log('info', `Processed ${agentName} model change in kilo-meta.json`);
}
}
} catch (error) {
log('error', `Failed to process kilo-meta.json changes: ${error.message}`);
}
}
// Export DB to JSON
function exportDbToJson() {
return new Promise((resolve, reject) => {
const exportScript = path.join(__dirname, 'export-db-to-json.cjs');
try {
// Check if export script exists
if (!fs.existsSync(exportScript)) {
log('error', `Export script not found: ${exportScript}`);
resolve();
return;
}
// Try to import and run the export function
const exportModule = require(exportScript);
if (typeof exportModule === 'function') {
log('info', 'Exporting database to JSON...');
exportModule();
log('info', 'Database exported to JSON successfully');
resolve();
} else {
// If that fails, try running as a child process
const { exec } = require('child_process');
exec(`node "${exportScript}"`, (error, stdout, stderr) => {
if (error) {
log('error', `Export script failed: ${error.message}`);
log('error', `stderr: ${stderr}`);
resolve();
} else {
log('info', 'Database exported to JSON successfully');
log('info', `stdout: ${stdout}`);
resolve();
}
});
}
} catch (error) {
log('error', `Failed to export database to JSON: ${error.message}`);
resolve(); // Don't fail the whole process if export fails
}
});
}
// Initialize database
function initializeDatabase() {
// If sqlite3 is not available, return a mock database object
if (!sqlite3) {
return Promise.resolve({
get: (sql, callback) => callback(null, null),
run: (sql, params, callback) => callback && callback(null),
close: (callback) => callback && callback(null)
});
}
return new Promise((resolve, reject) => {
const db = new sqlite3.Database(DB_PATH, (err) => {
if (err) {
reject(err);
return;
}
// Create tables if they don't exist
db.serialize(() => {
db.run(`CREATE TABLE IF NOT EXISTS meta (
key TEXT PRIMARY KEY,
value TEXT
)`);
db.run(`CREATE TABLE IF NOT EXISTS agents (
name TEXT PRIMARY KEY,
current_model TEXT,
description TEXT,
mode TEXT,
color TEXT,
file_path TEXT,
last_updated TEXT
)`);
db.run(`CREATE TABLE IF NOT EXISTS agent_versions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_name TEXT,
model TEXT,
date TEXT,
commit TEXT,
reason TEXT,
change_type TEXT,
score INTEGER
)`);
db.run(`CREATE TABLE IF NOT EXISTS recommendations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_name TEXT,
recommended_model TEXT,
impact TEXT,
source TEXT,
generated_at TEXT,
applied INTEGER DEFAULT 0
)`);
resolve(db);
});
});
});
}
// Main sync function
async function syncDatabase(db) {
try {
// Get last synced commit
const lastSyncedCommit = await getLastSyncedCommit(db);
log('info', `Last synced commit: ${lastSyncedCommit || 'none (first run)'}`);
// Get current commit
const currentCommit = getCurrentCommit();
if (!currentCommit) {
log('error', 'Could not determine current commit');
return;
}
log('info', `Current commit: ${currentCommit}`);
// If this is the first run, bootstrap with current commit
if (!lastSyncedCommit) {
log('info', 'First run, bootstrapping with current commit');
await updateLastSyncedCommit(db, currentCommit);
return;
}
// Check if there are changes
if (currentCommit === lastSyncedCommit) {
log('info', 'No changes detected');
return;
}
log('info', 'Changes detected, processing...');
// Get changed files
const changedFiles = getChangedFiles(lastSyncedCommit);
log('info', `Changed files: ${changedFiles.join(', ')}`);
// Get commit info
const commitInfo = {
hash: currentCommit,
date: new Date().toISOString(),
message: 'Incremental update'
};
// Process each changed file
for (const file of changedFiles) {
if (file.endsWith('.md') && file.startsWith('.kilo/agents/')) {
await processAgentFile(db, file, commitInfo);
} else if (file === 'kilo-meta.json') {
await processKiloMetaChanges(db, commitInfo);
}
}
// Update last synced commit
await updateLastSyncedCommit(db, currentCommit);
// Export to JSON
await exportDbToJson();
log('info', 'Database sync completed successfully');
} catch (error) {
log('error', `Database sync failed: ${error.message}`);
}
}
// Main function
async function main() {
let db;
try {
log('info', 'Starting agent evolution DB watcher');
// Initialize database
db = await initializeDatabase();
log('info', `Database initialized at ${DB_PATH}`);
// Initial sync
await syncDatabase(db);
// Set up polling
log('info', `Starting poll loop with interval ${WATCH_INTERVAL_MS}ms`);
const intervalId = setInterval(async () => {
await syncDatabase(db);
}, WATCH_INTERVAL_MS);
// Handle graceful shutdown
const shutdown = async (signal) => {
log('info', `Received ${signal}, shutting down gracefully...`);
// Clear interval
clearInterval(intervalId);
// Close database
if (db) {
db.close((err) => {
if (err) {
log('error', `Error closing database: ${err.message}`);
} else {
log('info', 'Database closed successfully');
}
process.exit(0);
});
} else {
process.exit(0);
}
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
} catch (error) {
log('error', `Failed to start watcher: ${error.message}`);
if (db) {
db.close();
}
process.exit(1);
}
}
// Run main function if script is executed directly
if (require.main === module) {
main();
}
module.exports = {
syncDatabase,
initializeDatabase,
parseYamlFrontmatter,
getChangedFiles,
processAgentFile,
processKiloMetaChanges
};

View File

@@ -29,6 +29,7 @@
"evolution:reload": "bash agent-evolution/docker-run.sh reload",
"evolution:restart": "bash agent-evolution/docker-run.sh restart",
"evolution:stop": "bash agent-evolution/docker-run.sh stop",
"evolution:watch": "node agent-evolution/scripts/watch-db.cjs",
"agent:stats": "bun run scripts/agent-stats.ts",
"agent:stats:week": "bun run scripts/agent-stats.ts --last 7",
"agent:stats:project": "bun run scripts/agent-stats.ts --project",