automagik-genie
Version:
Self-evolving AI agent orchestration framework with Model Context Protocol support
245 lines (204 loc) • 5.77 kB
JavaScript
/**
* Bullet ID Generator
*
* Generates unique IDs for structured learning bullets.
* Format: [prefix-NNN] where NNN is zero-padded 3-digit counter.
*
* Usage:
* genie helper bullet-id file.md
* genie helper bullet-id file.md --count=10
* genie helper bullet-id file.md --peek (show next without incrementing)
*/
const fs = require('fs');
const path = require('path');
// Maximum bullet ID number (supports 001-999)
const MAX_BULLET_ID = 999;
/**
* Extract prefix from filename
* learn.md → learn
* orchestration-boundary-protocol.md → orchestration
*/
function getFilePrefix(filePath) {
const basename = path.basename(filePath, '.md');
// For multi-word files, use first word
const firstWord = basename.split('-')[0];
// Lowercase for consistency
return firstWord.toLowerCase();
}
/**
* Get cache directory path
*/
function getCacheDir() {
const cacheDir = path.join(process.cwd(), '.genie', '.cache', 'bullet-ids');
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
}
return cacheDir;
}
/**
* Get cache file path for prefix
*/
function getCachePath(prefix) {
return path.join(getCacheDir(), `${prefix}.json`);
}
/**
* Read last ID from cache
*/
function getLastIdFromCache(prefix) {
const cachePath = getCachePath(prefix);
if (!fs.existsSync(cachePath)) {
return 0;
}
try {
const cache = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
return cache.lastId || 0;
} catch (err) {
console.error(`Warning: Cache read failed for ${prefix}, starting from 0`);
return 0;
}
}
/**
* Scan file for highest existing ID
* Handles cases where file was manually edited
*/
function scanFileForHighestId(filePath, prefix) {
if (!fs.existsSync(filePath)) {
return 0;
}
try {
const content = fs.readFileSync(filePath, 'utf-8');
const pattern = new RegExp(`\\[${prefix}-(\\d{3})\\]`, 'g');
let highest = 0;
let match;
while ((match = pattern.exec(content)) !== null) {
const idNum = parseInt(match[1], 10);
if (idNum > highest) {
highest = idNum;
}
}
return highest;
} catch (err) {
console.error(`Warning: Could not scan ${filePath}`);
return 0;
}
}
/**
* Get next ID (reconciles cache and file scan)
*/
function getNextId(filePath, prefix) {
const cachedId = getLastIdFromCache(prefix);
const scannedId = scanFileForHighestId(filePath, prefix);
// Use whichever is higher (handles manual edits)
const lastId = Math.max(cachedId, scannedId);
return lastId + 1;
}
/**
* Save last ID to cache
*/
function saveIdToCache(prefix, lastId) {
const cachePath = getCachePath(prefix);
fs.writeFileSync(cachePath, JSON.stringify({
prefix,
lastId,
updated: new Date().toISOString()
}, null, 2));
}
/**
* Generate formatted ID
*/
function formatId(prefix, number) {
const paddedNum = String(number).padStart(3, '0');
return `${prefix}-${paddedNum}`;
}
/**
* Generate single ID
*/
function generateSingleId(filePath, peek = false) {
const prefix = getFilePrefix(filePath);
const nextNum = getNextId(filePath, prefix);
if (nextNum > MAX_BULLET_ID) {
console.error(`ERROR: ID limit reached for ${prefix} (max ${MAX_BULLET_ID})`);
process.exit(1);
}
const id = formatId(prefix, nextNum);
// Save to cache unless peek mode
if (!peek) {
saveIdToCache(prefix, nextNum);
}
return id;
}
/**
* Generate batch of IDs
*/
function generateBatchIds(filePath, count) {
const prefix = getFilePrefix(filePath);
let nextNum = getNextId(filePath, prefix);
if (nextNum + count > MAX_BULLET_ID) {
console.error(`ERROR: Batch would exceed ID limit for ${prefix} (max ${MAX_BULLET_ID})`);
process.exit(1);
}
const ids = [];
for (let i = 0; i < count; i++) {
ids.push(formatId(prefix, nextNum + i));
}
// Save final ID to cache
saveIdToCache(prefix, nextNum + count - 1);
return ids;
}
/**
* Main CLI
*/
async function main() {
const args = process.argv.slice(2);
// Help
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
console.log('Usage:');
console.log(' genie helper bullet-id file.md');
console.log(' Generate next ID for file');
console.log('');
console.log(' genie helper bullet-id file.md --count=10');
console.log(' Generate batch of 10 IDs');
console.log('');
console.log(' genie helper bullet-id file.md --peek');
console.log(' Show next ID without incrementing');
console.log('');
console.log('Examples:');
console.log(' $ genie helper bullet-id .genie/spells/learn.md');
console.log(' learn-042');
console.log('');
console.log(' $ genie helper bullet-id .genie/spells/learn.md --count=5');
console.log(' learn-042');
console.log(' learn-043');
console.log(' learn-044');
console.log(' learn-045');
console.log(' learn-046');
return;
}
const filePath = args[0];
const countArg = args.find(a => a.startsWith('--count='));
const peek = args.includes('--peek');
if (!filePath) {
console.error('Error: File path required');
console.error('Usage: genie helper bullet-id file.md');
process.exit(1);
}
// Batch generation
if (countArg) {
const count = parseInt(countArg.split('=')[1], 10);
if (isNaN(count) || count < 1) {
console.error('Error: Invalid count');
process.exit(1);
}
const ids = generateBatchIds(filePath, count);
ids.forEach(id => console.log(id));
return;
}
// Single ID generation
const id = generateSingleId(filePath, peek);
console.log(id);
}
main().catch(err => {
console.error('ERROR:', err.message);
process.exit(1);
});