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:
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>';
|
||||
});
|
||||
|
||||
520
agent-evolution/scripts/watch-db.cjs
Normal file
520
agent-evolution/scripts/watch-db.cjs
Normal 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
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user