141 lines
4.3 KiB
JavaScript
141 lines
4.3 KiB
JavaScript
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
// Curator — background skill maintenance orchestrator
|
||
// Periodically reviews agent-created skills and memories for
|
||
// consolidation, staleness, and merge opportunities.
|
||
class Curator {
|
||
constructor(config) {
|
||
this.config = config || {};
|
||
this.skillsDir = config.skillsDir || path.join(process.env.HOME || process.env.USERPROFILE || '.', '.hermes', 'skills');
|
||
this.reportDir = config.reportDir || path.join(__dirname, 'reports');
|
||
this.staleDays = config.staleDays || 30;
|
||
this.archiveDays = config.archiveDays || 90;
|
||
this.enabled = config.enabled !== false;
|
||
}
|
||
|
||
async run(memoryProvider) {
|
||
if (!this.enabled) return { status: 'disabled' };
|
||
|
||
var report = {
|
||
timestamp: new Date().toISOString(),
|
||
staleSkills: [],
|
||
archivedSkills: [],
|
||
mergedSuggestions: [],
|
||
memoryAnalysis: null
|
||
};
|
||
|
||
// 1. Check skill staleness
|
||
try {
|
||
var skills = this._scanSkills();
|
||
report.staleSkills = this._checkStaleness(skills);
|
||
report.archivedSkills = this._checkArchive(skills);
|
||
} catch (e) {
|
||
report.skillError = e.message;
|
||
}
|
||
|
||
// 2. Analyze memories for consolidation opportunities
|
||
if (memoryProvider) {
|
||
try {
|
||
report.memoryAnalysis = this._analyzeMemories(memoryProvider);
|
||
} catch (e) {
|
||
report.memoryError = e.message;
|
||
}
|
||
}
|
||
|
||
// 3. Generate report
|
||
this._saveReport(report);
|
||
|
||
return report;
|
||
}
|
||
|
||
_scanSkills() {
|
||
if (!fs.existsSync(this.skillsDir)) return [];
|
||
var entries = [];
|
||
try {
|
||
var items = fs.readdirSync(this.skillsDir, { withFileTypes: true });
|
||
for (var i = 0; i < items.length; i++) {
|
||
if (items[i].isDirectory() || items[i].name.endsWith('.md')) {
|
||
var fullPath = path.join(this.skillsDir, items[i].name);
|
||
var stat = fs.statSync(fullPath);
|
||
entries.push({
|
||
name: items[i].name,
|
||
path: fullPath,
|
||
isDir: items[i].isDirectory(),
|
||
created: stat.birthtime,
|
||
modified: stat.mtime,
|
||
ageDays: (Date.now() - stat.mtime.getTime()) / 86400000
|
||
});
|
||
}
|
||
}
|
||
} catch (e) {}
|
||
return entries;
|
||
}
|
||
|
||
_checkStaleness(skills) {
|
||
return skills.filter(function(s) {
|
||
return s.ageDays > this.staleDays && s.ageDays < this.archiveDays;
|
||
}.bind(this)).map(function(s) {
|
||
return { name: s.name, ageDays: Math.round(s.ageDays), action: 'mark-stale' };
|
||
});
|
||
}
|
||
|
||
_checkArchive(skills) {
|
||
return skills.filter(function(s) {
|
||
return s.ageDays > this.archiveDays;
|
||
}.bind(this)).map(function(s) {
|
||
return { name: s.name, ageDays: Math.round(s.ageDays), action: 'archive' };
|
||
});
|
||
}
|
||
|
||
_analyzeMemories(memoryProvider) {
|
||
var stats = memoryProvider.getStats();
|
||
var suggestions = [];
|
||
|
||
// Check for memory type balance
|
||
if (stats.byType) {
|
||
var total = stats.total || 0;
|
||
if (total > 50) {
|
||
suggestions.push('记忆总数超过50条,建议整理合并相似条目');
|
||
}
|
||
var taskResults = stats.byType.task_result || 0;
|
||
if (taskResults > 20) {
|
||
suggestions.push('task_result 类型记忆过多(' + taskResults + '条),建议按项目归类');
|
||
}
|
||
}
|
||
|
||
return {
|
||
totalMemories: stats.total || 0,
|
||
byType: stats.byType || {},
|
||
suggestions: suggestions
|
||
};
|
||
}
|
||
|
||
_saveReport(report) {
|
||
if (!fs.existsSync(this.reportDir)) fs.mkdirSync(this.reportDir, { recursive: true });
|
||
var ts = new Date().toISOString().replace(/[:.]/g, '-');
|
||
var reportPath = path.join(this.reportDir, 'curator-' + ts + '.json');
|
||
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf8');
|
||
}
|
||
|
||
getReportHistory(limit) {
|
||
if (limit === undefined) limit = 5;
|
||
if (!fs.existsSync(this.reportDir)) return [];
|
||
try {
|
||
var files = fs.readdirSync(this.reportDir)
|
||
.filter(function(f) { return f.startsWith('curator-') && f.endsWith('.json'); })
|
||
.sort()
|
||
.reverse()
|
||
.slice(0, limit);
|
||
var reports = [];
|
||
for (var i = 0; i < files.length; i++) {
|
||
try {
|
||
reports.push(JSON.parse(fs.readFileSync(path.join(this.reportDir, files[i]), 'utf8')));
|
||
} catch (e) {}
|
||
}
|
||
return reports;
|
||
} catch (e) { return []; }
|
||
}
|
||
}
|
||
|
||
module.exports = Curator; |