automagik-genie
Version:
Self-evolving AI agent orchestration framework with Model Context Protocol support
545 lines (544 loc) • 25.7 kB
JavaScript
;
/**
* Genie CLI - Unified command-line interface with commander.js
*
* Provides a unified CLI for:
* - Agent orchestration (run, resume, list, view, stop)
* - MCP server management (genie mcp)
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const commander_1 = require("commander");
const child_process_1 = require("child_process");
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const gradient_string_1 = __importDefault(require("gradient-string"));
const cli_utils_1 = require("./lib/cli-utils");
const router_1 = require("./lib/router");
const server_manager_1 = require("./lib/server-manager");
const mcp_stdio_1 = require("./lib/mcp-stdio");
const mcp_http_1 = require("./lib/mcp-http");
const spell_changelog_1 = require("./lib/spell-changelog");
const program = new commander_1.Command();
// Universe Genie-themed gradients 🧞✨🌌
const cosmicGradient = (0, gradient_string_1.default)(['#4169e1', '#8a2be2', '#ff1493']); // Royal Blue → Blue Violet → Deep Pink
const performanceGradient = (0, gradient_string_1.default)(['#ffd700', '#ff8c00', '#ff6347']); // Gold → Orange → Tomato
const successGradient = (0, gradient_string_1.default)(['#00ff88', '#00ccff', '#0099ff']); // Green → Cyan → Sky Blue
const magicGradient = (0, gradient_string_1.default)(['#ff00ff', '#9933ff', '#0066ff']); // Fuscia → Purple → Blue (reverse)
// Get package version
const packageJson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '../../package.json'), 'utf8'));
program
.name('genie')
.description('Self-evolving AI agent orchestration framework\n\nRun with no arguments to start Genie server (Forge + MCP)')
.version(packageJson.version)
.option('--debug', 'Enable debug mode (logs all incoming requests to MCP server)');
// ==================== SERVER MANAGEMENT ====================
// Status command
program
.command('status')
.description('Show Genie server status (Forge backend, MCP server, statistics)')
.action(() => {
(0, cli_utils_1.execGenie)(['status']);
});
// Dashboard command
program
.command('dashboard')
.description('Show live engagement statistics dashboard')
.option('--watch', 'Live mode with real-time updates')
.action((options) => {
const args = ['dashboard'];
if (options.watch) {
args.push('--watch');
}
(0, cli_utils_1.execGenie)(args);
});
// MCP command group (stdio and http modes)
const mcpCommand = program
.command('mcp')
.description('MCP server management (stdio for Claude Desktop, http for headless deployment)');
// MCP stdio subcommand (default behavior for backward compatibility)
mcpCommand
.command('stdio', { isDefault: true })
.description('Start MCP server in stdio mode (for Claude Desktop). Requires Forge to be running.')
.action(mcp_stdio_1.startMCPStdio);
// MCP http subcommand (new headless mode)
mcpCommand
.command('http')
.description('Start MCP server in HTTP mode (headless, non-interactive)')
.option('-p, --port <port>', 'Port to listen on (defaults to MCP_PORT env or 8885)')
.option('-d, --debug', 'Enable debug mode (verbose logging)')
.action((options) => {
(0, mcp_http_1.startMCPHttp)({
port: options.port ? parseInt(options.port, 10) : undefined,
debug: options.debug
});
});
// ==================== AGENT ORCHESTRATION ====================
// Run command (unified browser + monitoring)
program
.command('run <agent> <prompt>')
.description('Run agent with browser UI and live monitoring (outputs JSON on completion)')
.option('-b, --background', 'Run in background mode (uses headless task instead)')
.option('-x, --executor <executor>', 'Override executor for this run')
.option('-m, --model <model>', 'Override model for the selected executor')
.option('-n, --name <name>', 'Friendly session name for easy identification')
.option('--raw', 'Output raw text only (no JSON)')
.option('--quiet', 'Suppress startup messages')
.action((agent, prompt, options) => {
const args = ['run', agent, prompt];
if (options.background === true) {
args.push('--background');
}
if (options.background === false) {
args.push('--no-background');
}
if (options.executor) {
args.push('--executor', options.executor);
}
if (options.model) {
args.push('--model', options.model);
}
if (options.name) {
args.push('--name', options.name);
}
if (options.raw) {
args.push('--raw');
}
if (options.quiet) {
args.push('--quiet');
}
(0, cli_utils_1.execGenie)(args);
});
// Task command (headless execution)
program
.command('task <agent> <prompt>')
.description('Run agent task headlessly (no browser, immediate return with task ID)')
.option('-x, --executor <executor>', 'Override executor for this task')
.option('-m, --model <model>', 'Override model for the selected executor')
.option('-n, --name <name>', 'Friendly session name for easy identification')
.option('--raw', 'Output raw attempt ID only (no JSON)')
.option('--quiet', 'Suppress startup messages')
.action((agent, prompt, options) => {
const args = ['task', agent, prompt];
if (options.executor) {
args.push('--executor', options.executor);
}
if (options.model) {
args.push('--model', options.model);
}
if (options.name) {
args.push('--name', options.name);
}
if (options.raw) {
args.push('--raw');
}
if (options.quiet) {
args.push('--quiet');
}
(0, cli_utils_1.execGenie)(args);
});
// Talk command (deprecated, use 'run' instead)
program
.command('talk <agent>')
.description('[Deprecated] Start interactive browser task with agent (use "genie run" instead)')
.action((agent) => {
(0, cli_utils_1.execGenie)(['talk', agent]);
});
// Resume command
program
.command('resume <taskId> <prompt>')
.description('Resume an existing agent task')
.action((taskId, prompt) => {
(0, cli_utils_1.execGenie)(['resume', taskId, prompt]);
});
// List command
program
.command('list [type]')
.description('List agents, tasks, or workflows')
.action((type) => {
const normalized = (type || 'agents').toLowerCase();
const aliasMap = {
session: 'tasks',
sessions: 'tasks',
task: 'tasks',
agent: 'agents'
};
const resolved = aliasMap[normalized] || normalized;
const validTypes = new Set(['agents', 'collectives', 'tasks', 'workflows', 'skills']);
if (!validTypes.has(resolved)) {
console.error('Error: list command accepts agents (default), tasks, workflows, skills, or collectives');
process.exit(1);
}
(0, cli_utils_1.execGenie)(['list', resolved]);
});
// View command
program
.command('view <taskId>')
.description('View task transcript')
.option('--full', 'Show full transcript')
.option('--live', 'Live view (auto-refresh)')
.action((taskId, options) => {
const args = ['view', taskId];
if (options.full)
args.push('--full');
if (options.live)
args.push('--live');
(0, cli_utils_1.execGenie)(args);
});
// Stop command
program
.command('stop <taskId>')
.description('Stop a running task')
.action((taskId) => {
(0, cli_utils_1.execGenie)(['stop', taskId]);
});
// ==================== WORKSPACE MANAGEMENT ====================
// Init command
program
.command('init [template]')
.description('Initialize Genie configuration in the current workspace')
.option('-y, --yes', 'Accept defaults without prompting')
.action((template, options) => {
const args = ['init'];
if (template) {
args.push(template);
}
if (options.yes) {
args.push('--yes');
}
(0, cli_utils_1.execGenie)(args);
});
// Rollback command
program
.command('rollback')
.description('Restore a previous Genie backup snapshot')
.option('--list', 'List available backups')
.option('--latest', 'Restore the most recent backup')
.option('--id <backupId>', 'Restore a specific backup by ID')
.action((options) => {
const args = ['rollback'];
if (options.list) {
args.push('--list');
}
if (options.latest) {
args.push('--latest');
}
if (options.id) {
args.push('--id', options.id);
}
(0, cli_utils_1.execGenie)(args);
});
// ==================== PACKAGE MANAGEMENT ====================
// Update command (npm package with changelog from GitHub)
program
.command('update')
.description('Sync with the collective (your Genie absorbs new magik next time you run `genie`)')
.option('--force', 'Skip confirmation prompts')
.action((options) => {
const args = ['update'];
if (options.force) {
args.push('--force');
}
(0, cli_utils_1.execGenie)(args);
});
// ==================== HELPER UTILITIES ====================
// Helper command (access to utility scripts)
program
.command('helper [command] [args...]')
.description('Run Genie helper utilities (count-tokens, validate-frontmatter, etc.)')
.allowUnknownOption()
.action((command, args) => {
const helperRouter = path_1.default.join(process.cwd(), '.genie', 'scripts', 'helpers', 'index.js');
// Check if helper router exists
if (!fs_1.default.existsSync(helperRouter)) {
console.error('Error: Helper utilities not found. Run genie init to set up your workspace.');
process.exit(1);
}
const helperArgs = command ? [command, ...(args || [])] : [];
const child = (0, child_process_1.spawn)('node', [helperRouter, ...helperArgs], {
stdio: 'inherit',
env: process.env
});
child.on('exit', (code) => {
process.exit(code || 0);
});
child.on('error', (err) => {
console.error(`Failed to run helper: ${err.message}`);
process.exit(1);
});
});
// Version check for ALL commands (prevents outdated users from bypassing init)
const args = process.argv.slice(2);
// Skip version check for these commands (they're safe to run with any version)
const skipVersionCheck = ['--version', '-V', '--help', '-h', 'update', 'init', 'rollback', 'mcp'];
// Non-blocking version check for these commands (show warning but continue)
const nonBlockingCommands = ['list', 'status', 'dashboard', 'view', 'helper', 'run', 'talk', 'task', 'resume', 'stop'];
// Skip version check for specific agents/spells that need to run regardless of version
// WHY: Learn spell loads for self-enhancement, install/update handle versions themselves
const BYPASS_VERSION_CHECK_AGENTS = ['learn', 'install', 'update', 'upstream-update'];
const isRunCommand = args[0] === 'run';
const agentName = args[1];
const shouldBypassForAgent = isRunCommand && BYPASS_VERSION_CHECK_AGENTS.includes(agentName);
const shouldCheckVersion = args.length > 0 &&
!skipVersionCheck.some(cmd => args.includes(cmd)) &&
!shouldBypassForAgent;
const isNonBlockingCommand = nonBlockingCommands.includes(args[0]);
const skipVersionGate = process.env.GENIE_SKIP_VERSION_CHECK === '1';
if (shouldCheckVersion) {
// Check if version.json exists and matches current version
const genieDir = path_1.default.join(process.cwd(), '.genie');
const versionPath = path_1.default.join(genieDir, 'state', 'version.json');
const hasGenieConfig = fs_1.default.existsSync(genieDir);
// MASTER GENIE DETECTION: Check if we're in THE SOURCE template repo
// (not just any repo named automagik-genie, but THE ACTUAL UPSTREAM)
const workspacePackageJson = path_1.default.join(process.cwd(), 'package.json');
let isMasterGenie = false;
if (fs_1.default.existsSync(workspacePackageJson)) {
try {
const workspacePkg = JSON.parse(fs_1.default.readFileSync(workspacePackageJson, 'utf8'));
if (workspacePkg.name === 'automagik-genie') {
// Additional check: Verify this is THE upstream source repo
const { execSync } = require('child_process');
try {
const remoteUrl = execSync('git config --get remote.origin.url', {
encoding: 'utf8',
cwd: process.cwd(),
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
// Only the ACTUAL source repo (namastexlabs/automagik-genie)
if (remoteUrl.includes('namastexlabs/automagik-genie') ||
remoteUrl.includes('automagik-genie/genie')) {
isMasterGenie = true;
}
}
catch {
// No git remote or command failed - not master genie
}
}
}
catch {
// Not master genie if can't read package.json
}
}
// If master genie and version mismatch, need to install locally built package globally
if (isMasterGenie && hasGenieConfig && fs_1.default.existsSync(versionPath)) {
try {
const versionData = JSON.parse(fs_1.default.readFileSync(versionPath, 'utf8'));
const localVersion = versionData.version;
const runningVersion = packageJson.version;
if (localVersion !== runningVersion && !skipVersionGate) {
// Compare versions to determine which is newer
const compareVersions = (a, b) => {
const aParts = a.replace(/^v/, '').split('-');
const bParts = b.replace(/^v/, '').split('-');
const aBase = aParts[0].split('.').map(Number);
const bBase = bParts[0].split('.').map(Number);
for (let i = 0; i < Math.max(aBase.length, bBase.length); i++) {
const aNum = aBase[i] || 0;
const bNum = bBase[i] || 0;
if (aNum > bNum)
return 1;
if (aNum < bNum)
return -1;
}
// If base versions equal, compare prerelease (rc.X)
if (aParts[1] && bParts[1]) {
const aRc = parseInt(aParts[1].replace('rc.', '')) || 0;
const bRc = parseInt(bParts[1].replace('rc.', '')) || 0;
return aRc - bRc;
}
return 0;
};
const localIsNewer = compareVersions(localVersion, runningVersion) > 0;
// Non-blocking commands: show warning but continue
if (isNonBlockingCommand) {
if (localIsNewer) {
console.log('');
console.log(performanceGradient('⚠️ Workspace ahead: ') + `${performanceGradient('global ' + runningVersion)} → ${successGradient('workspace ' + localVersion)}`);
console.log(' Run ' + performanceGradient('genie update') + ' to install your new build globally');
console.log('');
}
else {
console.log('');
console.log(performanceGradient('⚠️ Workspace behind: ') + `${performanceGradient('workspace ' + localVersion)} ← ${successGradient('global ' + runningVersion)}`);
console.log(' Run ' + performanceGradient('genie') + ' to sync workspace');
console.log('');
}
}
else {
// Blocking commands: require sync before proceeding
if (localIsNewer) {
console.log('');
console.log(successGradient('━'.repeat(60)));
console.log(successGradient(' 🧞 ✨ NEW VERSION READY! ✨ 🧞 '));
console.log(successGradient('━'.repeat(60)));
console.log('');
console.log(`Your workspace has ${successGradient(localVersion)} but global is ${performanceGradient(runningVersion)}`);
console.log('');
console.log('Install your new build globally:');
console.log(' ' + performanceGradient('pnpm install -g .'));
console.log('');
console.log('Or use:');
console.log(' ' + performanceGradient('genie update') + ' (detects master genie and installs local build)');
console.log('');
process.exit(0);
}
else {
console.log('');
console.log(performanceGradient('━'.repeat(60)));
console.log(performanceGradient(' ⚠️ WORKSPACE OUTDATED'));
console.log(performanceGradient('━'.repeat(60)));
console.log('');
console.log(`Workspace: ${performanceGradient(localVersion)} (old)`);
console.log(`Global: ${successGradient(runningVersion)} (current)`);
console.log('');
console.log('Sync your workspace with global:');
console.log(' ' + successGradient('genie'));
console.log('');
process.exit(0);
}
}
}
else if (localVersion !== runningVersion && skipVersionGate) {
console.log('');
console.log(performanceGradient('⚠️ Version mismatch suppressed via GENIE_SKIP_VERSION_CHECK. Workspace ') + `${performanceGradient(localVersion)} vs global ${successGradient(runningVersion)}`);
console.log('');
}
}
catch {
// Ignore version check errors for master genie
}
}
if (hasGenieConfig && !fs_1.default.existsSync(versionPath)) {
// LOOPHOLE CLOSED: User has .genie but no version.json (pre-version-tracking)
// Redirect to init to create version.json
console.log(cosmicGradient('━'.repeat(60)));
console.log(magicGradient(' 🧞 ✨ VERSION TRACKING UPGRADE NEEDED ✨ 🧞 '));
console.log(cosmicGradient('━'.repeat(60)));
console.log('');
console.log('Your Genie installation needs to be upgraded to support version tracking.');
console.log('This is a one-time upgrade that will:');
console.log('');
console.log(successGradient('✓') + ' Backup your existing .genie directory');
console.log(successGradient('✓') + ' Enable automatic version detection');
console.log(successGradient('✓') + ' Preserve all your wishes, reports, and state');
console.log('');
console.log('Running init to upgrade...');
console.log('');
// Interactive if TTY available, otherwise use --yes
const initArgs = process.stdout.isTTY ? ['init'] : ['init', '--yes'];
(0, cli_utils_1.execGenie)(initArgs);
process.exit(0);
}
else if (hasGenieConfig && fs_1.default.existsSync(versionPath)) {
// Check version mismatch
try {
const versionData = JSON.parse(fs_1.default.readFileSync(versionPath, 'utf8'));
const installedVersion = versionData.version;
const currentVersion = packageJson.version;
if (installedVersion !== currentVersion) {
// Non-blocking commands: show warning but continue
if (isNonBlockingCommand) {
console.log('');
console.log(performanceGradient('⚠️ Update available: ') + `${performanceGradient(installedVersion)} → ${successGradient(currentVersion)}`);
console.log(' Run ' + performanceGradient('genie') + ' (without commands) to upgrade');
console.log('');
}
else {
// Blocking commands: Version mismatch detected - run init
console.log(cosmicGradient('━'.repeat(60)));
console.log(magicGradient(' 🧞 ✨ VERSION UPDATE REQUIRED ✨ 🧞 '));
console.log(cosmicGradient('━'.repeat(60)));
console.log('');
console.log(`Your Genie: ${successGradient(installedVersion)}`);
console.log(`The Collective: ${performanceGradient(currentVersion)} ⭐ NEW!`);
console.log('');
// Show new spells learned
const fromTag = (0, spell_changelog_1.getTagForVersion)(installedVersion);
const toTag = (0, spell_changelog_1.getTagForVersion)(currentVersion);
if (fromTag && toTag) {
const spellChangelog = (0, spell_changelog_1.getLearnedSpells)(fromTag, toTag);
if (spellChangelog.totalCount > 0) {
const spellLines = (0, spell_changelog_1.formatSpellChangelog)(spellChangelog);
spellLines.forEach(line => console.log(line));
}
else {
console.log('The collective has learned new magik!');
}
}
else {
console.log('The collective has learned new magik!');
}
console.log('⚡ Syncing new capabilities to your local clone...');
console.log('');
console.log(successGradient('✓') + ' Your existing .genie will be backed up automatically');
console.log(successGradient('✓') + ' All data stays local - nothing leaves your machine');
console.log('');
// Interactive if TTY available, otherwise use --yes
const initArgs = process.stdout.isTTY ? ['init'] : ['init', '--yes'];
(0, cli_utils_1.execGenie)(initArgs);
process.exit(0);
}
}
}
catch (error) {
// Corrupted version.json - force init
console.log(cosmicGradient('━'.repeat(60)));
console.log(magicGradient(' 🧞 ✨ VERSION FILE REPAIR NEEDED ✨ 🧞 '));
console.log(cosmicGradient('━'.repeat(60)));
console.log('');
console.log('Version file is corrupted. Repairing...');
console.log(successGradient('✓') + ' Your existing .genie will be backed up automatically');
console.log('');
// Interactive if TTY available, otherwise use --yes
const initArgs = process.stdout.isTTY ? ['init'] : ['init', '--yes'];
(0, cli_utils_1.execGenie)(initArgs);
process.exit(0);
}
}
// FIX for issue #237: Escape hatch - detect infinite loop scenario
// If version is STILL mismatched after attempting init, don't loop infinitely
// This can happen if init returns early (e.g., for 'old_genie' without --yes)
if (fs_1.default.existsSync(versionPath)) {
try {
const versionData = JSON.parse(fs_1.default.readFileSync(versionPath, 'utf8'));
const installedVersion = versionData.version;
const currentVersion = packageJson.version;
if (installedVersion !== currentVersion && shouldCheckVersion && !isNonBlockingCommand) {
// ESCAPE HATCH: Version didn't update during the version check phase (only for blocking commands)
// This means detectInstallType() triggered old_genie without init fully running
console.log('');
console.log(cosmicGradient('━'.repeat(60)));
console.log(magicGradient(' 🧞 ✨ LOOPHOLE DETECTED & CLOSED ✨ 🧞 '));
console.log(cosmicGradient('━'.repeat(60)));
console.log('');
console.log('⚠️ Version file did not update after init!');
console.log(` Expected: ${currentVersion}`);
console.log(` Got: ${installedVersion}`);
console.log('');
console.log('This usually means init returned early (old_genie detection).');
console.log('The fix has been applied - please try again.');
console.log('');
process.exit(1);
}
}
catch (error) {
// If version check fails here, safe to continue
}
}
}
// Extract global options manually before routing
const hasDebugFlag = args.includes('--debug');
// If no command was provided, use smart router
if (!args.length || (args.length === 1 && hasDebugFlag)) {
// No command (or only --debug flag) → call smartRouter()
// Pass debug flag explicitly since program.parse() hasn't been called yet
(0, router_1.smartRouter)(packageJson.version, hasDebugFlag, server_manager_1.startGenieServer);
}
else {
// Command provided → parse with commander
program.parse(process.argv);
}