automagik-genie
Version:
Self-evolving AI agent orchestration framework with Model Context Protocol support
272 lines (271 loc) • 11.4 kB
JavaScript
;
/**
* Server helper utilities
* Extracted from server.ts per Amendment 10 (file size discipline)
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.findWorkspaceRoot = findWorkspaceRoot;
exports.listAgents = listAgents;
exports.loadForgeExecutor = loadForgeExecutor;
exports.listTasks = listTasks;
exports.getGenieVersion = getGenieVersion;
exports.getVersionHeader = getVersionHeader;
exports.syncAgentProfilesToForge = syncAgentProfilesToForge;
exports.loadOAuth2Config = loadOAuth2Config;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const display_transform_js_1 = require("./display-transform.js");
// Find actual workspace root by searching upward for .genie/ directory
function findWorkspaceRoot() {
let dir = process.cwd();
while (dir !== path_1.default.dirname(dir)) {
if (fs_1.default.existsSync(path_1.default.join(dir, '.genie'))) {
return dir;
}
dir = path_1.default.dirname(dir);
}
// Fallback to process.cwd() if .genie not found
return process.cwd();
}
// Helper: List available agents from all collectives
function listAgents() {
const workspaceRoot = findWorkspaceRoot();
const agents = [];
// ONLY scan specific agents/ directories (not workflows/ or spells/)
const searchDirs = [
path_1.default.join(workspaceRoot, '.genie/code/agents'),
path_1.default.join(workspaceRoot, '.genie/create/agents')
];
const visit = (dirPath, relativePath) => {
const entries = fs_1.default.readdirSync(dirPath, { withFileTypes: true });
entries.forEach((entry) => {
const entryPath = path_1.default.join(dirPath, entry.name);
if (entry.isDirectory()) {
// Recurse into subdirectories (for sub-agents like git/workflows/, wish/)
visit(entryPath, relativePath ? path_1.default.join(relativePath, entry.name) : entry.name);
return;
}
if (!entry.isFile() || !entry.name.endsWith('.md') || entry.name === 'README.md') {
return;
}
const rawId = relativePath ? path_1.default.join(relativePath, entry.name) : entry.name;
const normalizedId = rawId.replace(/\.md$/i, '').split(path_1.default.sep).join('/');
// Extract frontmatter to get name and description
const content = fs_1.default.readFileSync(entryPath, 'utf8');
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
let name = normalizedId;
let description;
if (frontmatterMatch) {
const frontmatter = frontmatterMatch[1];
const nameMatch = frontmatter.match(/name:\s*(.+)/);
const descMatch = frontmatter.match(/description:\s*(.+)/);
if (nameMatch)
name = nameMatch[1].trim();
if (descMatch)
description = descMatch[1].trim();
}
// Transform display path (strip template/category folders)
const { displayId, displayFolder } = (0, display_transform_js_1.transformDisplayPath)(normalizedId);
agents.push({ id: normalizedId, displayId, name, description, folder: displayFolder || undefined });
});
};
// Visit all search directories
searchDirs.forEach(baseDir => {
if (fs_1.default.existsSync(baseDir)) {
visit(baseDir, null);
}
});
return agents;
}
// Helper: Safely load Forge executor from dist (package) or src (repo)
function loadForgeExecutor() {
// Prefer compiled dist (works in published package)
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require('../../cli/lib/forge-executor');
}
catch (_distErr) {
// Fallback to TypeScript sources for local dev (within repo)
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require('../../../src/cli/lib/forge-executor');
}
catch (_srcErr) {
return null;
}
}
}
// Helper: List recent sessions (uses Forge API)
async function listTasks() {
const workspaceRoot = findWorkspaceRoot();
try {
// ALWAYS use Forge API for session listing (complete executor replacement)
const mod = loadForgeExecutor();
if (!mod || typeof mod.createForgeExecutor !== 'function') {
throw new Error('Forge executor unavailable (did you build the CLI?)');
}
const forgeExecutor = mod.createForgeExecutor();
const forgeSessions = await forgeExecutor.listTasks();
const sessions = forgeSessions.map((entry) => {
const created = entry.created || entry.created_at || 'unknown';
const updated = entry.updated || entry.updated_at || entry.lastUsed || created;
return {
id: entry.id || entry.taskId || 'unknown',
name: entry.name || entry.taskId || 'unknown',
agent: entry.agent || 'unknown',
status: entry.status || 'unknown',
created,
lastUsed: updated
};
});
// Filter: Show running sessions + recent completed (last 10)
// Fix Bug #5: Filter out stale sessions (>24 hours old with no recent activity)
const now = Date.now();
const STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000; // 24 hours
const running = sessions.filter((s) => {
const status = s.status === 'running' || s.status === 'starting';
if (!status)
return false;
// Check if session is stale (created >24h ago, no recent activity)
if (s.lastUsed !== 'unknown') {
const lastUsedTime = new Date(s.lastUsed).getTime();
const age = now - lastUsedTime;
if (age > STALE_THRESHOLD_MS) {
// Mark as stale but keep for manual cleanup
return false;
}
}
return true;
});
const completed = sessions
.filter((s) => s.status === 'completed')
.sort((a, b) => {
if (a.lastUsed === 'unknown')
return 1;
if (b.lastUsed === 'unknown')
return -1;
return new Date(b.lastUsed).getTime() - new Date(a.lastUsed).getTime();
})
.slice(0, 10);
// Combine and sort by lastUsed descending
const combined = [...running, ...completed].sort((a, b) => {
if (a.lastUsed === 'unknown')
return 1;
if (b.lastUsed === 'unknown')
return -1;
return new Date(b.lastUsed).getTime() - new Date(a.lastUsed).getTime();
});
if (combined.length === 0) {
return loadTasksFromLocalStore(workspaceRoot);
}
return combined;
}
catch (error) {
// Fallback to local tasks.json if Forge API fails
console.warn('Failed to fetch Forge sessions, falling back to local store');
return loadTasksFromLocalStore(workspaceRoot);
}
}
function loadTasksFromLocalStore(workspaceRoot) {
const tasksFile = path_1.default.join(workspaceRoot, '.genie/state/tasks.json');
if (!fs_1.default.existsSync(tasksFile)) {
return [];
}
try {
const content = fs_1.default.readFileSync(tasksFile, 'utf8');
const store = JSON.parse(content);
const sessions = Object.entries(store.sessions || {}).map(([key, entry]) => ({
id: key, // Use attempt ID as primary identifier for resume/view/stop commands
name: entry.name || key,
agent: entry.agent || key,
status: entry.status || 'unknown',
created: entry.created || 'unknown',
lastUsed: entry.lastUsed || entry.created || 'unknown'
}));
const now = Date.now();
const STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000; // 24 hours
const running = sessions.filter(s => {
const status = s.status === 'running' || s.status === 'starting' || s.status === 'background';
if (!status)
return false;
if (s.lastUsed !== 'unknown') {
const lastUsedTime = new Date(s.lastUsed).getTime();
const age = now - lastUsedTime;
if (age > STALE_THRESHOLD_MS) {
return false;
}
}
return true;
});
const completed = sessions
.filter(s => s.status === 'completed')
.sort((a, b) => {
if (a.lastUsed === 'unknown')
return 1;
if (b.lastUsed === 'unknown')
return -1;
return new Date(b.lastUsed).getTime() - new Date(a.lastUsed).getTime();
})
.slice(0, 10);
return [...running, ...completed].sort((a, b) => {
if (a.lastUsed === 'unknown')
return 1;
if (b.lastUsed === 'unknown')
return -1;
return new Date(b.lastUsed).getTime() - new Date(a.lastUsed).getTime();
});
}
catch (error) {
return [];
}
}
// Helper: Get Genie version from package.json
function getGenieVersion() {
try {
// From dist/mcp/lib/server-helpers.js → root package.json (3 levels up)
const packageJsonPath = path_1.default.join(__dirname, '../../..', 'package.json');
const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf8'));
return packageJson.version || '0.0.0';
}
catch (error) {
return '0.0.0';
}
}
// Helper: Get version header for MCP outputs
function getVersionHeader() {
return `Genie MCP v${getGenieVersion()}\n\n`;
}
// NOTE: Agent profile sync removed - Forge discovers .genie folders natively
async function syncAgentProfilesToForge() {
// No-op: Forge discovers .genie folders natively, sync no longer needed
}
// Load OAuth2 configuration (if available)
function loadOAuth2Config() {
const workspaceRoot = findWorkspaceRoot();
// Try multiple locations for config-manager.js
const searchPaths = [
// 1. User workspace (for dev mode)
path_1.default.join(workspaceRoot, 'dist', 'cli', 'lib', 'config-manager.js'),
// 2. Global install (resolved relative to this MCP server file)
// MCP server at: node_modules/automagik-genie/dist/mcp/lib/server-helpers.js
// CLI at: node_modules/automagik-genie/dist/cli/lib/config-manager.js
path_1.default.join(__dirname, '../../cli/lib/config-manager.js'),
];
for (const configModPath of searchPaths) {
try {
if (fs_1.default.existsSync(configModPath)) {
const { loadOAuth2Config } = require(configModPath);
return loadOAuth2Config();
}
}
catch (error) {
// Try next path
continue;
}
}
// Config not available (expected for stdio transport)
return null;
}