UNPKG

automagik-genie

Version:

Self-evolving AI agent orchestration framework with Model Context Protocol support

356 lines (355 loc) 14 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.listAgents = listAgents; exports.listCollectives = listCollectives; exports.resolveAgentIdentifier = resolveAgentIdentifier; exports.agentExists = agentExists; exports.loadAgentSpec = loadAgentSpec; exports.extractFrontMatter = extractFrontMatter; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const config_1 = require("./config"); const display_transform_1 = require("./display-transform"); let YAML = null; try { // eslint-disable-next-line @typescript-eslint/no-var-requires YAML = require('yaml'); } catch (_) { // yaml module optional } const fallbackParseFrontMatter = (raw) => { const meta = {}; raw.split(/\r?\n/).forEach((line) => { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) return; const separatorIndex = trimmed.indexOf(':'); if (separatorIndex === -1) return; const key = trimmed.slice(0, separatorIndex).trim(); const value = trimmed.slice(separatorIndex + 1).trim(); if (!key) return; meta[key] = value; }); return meta; }; const COLLECTIVE_MARKER = 'AGENTS.md'; const AGENT_DIRECTORY_NAME = 'agents'; function realpathOrNull(target) { try { return fs_1.default.realpathSync(target); } catch { return null; } } function toAgentPathSegments(id) { if (!id) return null; const segments = id .split('/') .map((segment) => segment.trim()) .filter(Boolean); if (!segments.length) return null; if (segments.some((segment) => segment === '.' || segment === '..')) { return null; } return segments; } function discoverCollectiveDirectories(genieRoot, maxDepth = 1) { const discovered = new Map(); if (!fs_1.default.existsSync(genieRoot) || !fs_1.default.statSync(genieRoot).isDirectory()) { return []; } const queue = [{ dir: genieRoot, depth: 0 }]; while (queue.length) { const { dir, depth } = queue.shift(); const markerPath = path_1.default.join(dir, COLLECTIVE_MARKER); if (fs_1.default.existsSync(markerPath)) { const resolvedRoot = realpathOrNull(dir) ?? path_1.default.resolve(dir); const relative = path_1.default.relative(genieRoot, resolvedRoot); const segments = relative.split(path_1.default.sep).filter(Boolean); const collective = segments.length ? segments.join('/') : 'root'; let agentsDir = null; const candidateAgentsDir = path_1.default.join(dir, AGENT_DIRECTORY_NAME); if (fs_1.default.existsSync(candidateAgentsDir) && fs_1.default.statSync(candidateAgentsDir).isDirectory()) { agentsDir = realpathOrNull(candidateAgentsDir) ?? path_1.default.resolve(candidateAgentsDir); } if (!discovered.has(resolvedRoot)) { discovered.set(resolvedRoot, { collective, root: resolvedRoot, agentsDir }); } // Do not traverse beyond located collectives; they manage their own hierarchy } if (depth >= maxDepth) continue; const entries = fs_1.default.readdirSync(dir, { withFileTypes: true }); entries.forEach((entry) => { if (!entry.isDirectory()) return; if (entry.name.startsWith('.')) return; const nextDir = path_1.default.join(dir, entry.name); queue.push({ dir: nextDir, depth: depth + 1 }); }); } return Array.from(discovered.values()); } function getLocalCollectives() { const dirs = new Map(); const genieRoot = path_1.default.join(process.cwd(), '.genie'); discoverCollectiveDirectories(genieRoot, 2).forEach((info) => { dirs.set(info.root, info); }); return Array.from(dirs.values()); } function findAgentFile(id, collectives) { const segments = toAgentPathSegments(id); if (!segments) return null; for (const info of collectives) { if (!info.agentsDir) continue; const candidateSegments = []; candidateSegments.push(segments); if (segments.length > 1 && segments[0] === info.collective) { candidateSegments.push(segments.slice(1)); if (segments[1] === 'agents') { candidateSegments.push(segments.slice(2)); } } if (segments[0] === 'agents') { candidateSegments.push(segments.slice(1)); } const seen = new Set(); for (const candidate of candidateSegments) { if (!candidate.length) continue; const key = candidate.join('/'); if (seen.has(key)) continue; seen.add(key); const filePath = path_1.default.join(info.agentsDir, ...candidate) + '.md'; if (fs_1.default.existsSync(filePath) && fs_1.default.statSync(filePath).isFile()) { const relativeId = candidate.join('/').replace(/\\/g, '/'); return { path: filePath, collective: info.collective, relativeId }; } } } return null; } const resolveAgentPath = (id) => { const normalized = id.replace(/\\/g, '/'); const collectives = getLocalCollectives().filter(info => Boolean(info.agentsDir)); const candidates = new Set([normalized]); if (!normalized.includes('/')) { collectives .map(info => info.collective) .filter((name) => Boolean(name) && name !== 'root') .forEach((collectiveName) => { candidates.add(`${collectiveName}/${normalized}`); }); } // Check local collectives first (user project - takes precedence) for (const candidate of candidates) { const found = findAgentFile(candidate, collectives); if (found) { const relativeId = found.relativeId; return found.collective && found.collective !== 'root' ? `${found.collective}/${relativeId}` : relativeId; } } return null; }; // transformDisplayPath imported from ./display-transform (single source of truth) /** * Lists all available agent definitions from both local and npm package locations. * * Recursively scans for .md files, extracts metadata, and filters out hidden/disabled agents. * Checks local collectives (directories with AGENTS.md + agents/) first, then the packaged core agents. * * @returns {ListedAgent[]} - Array of agent records with id, label, metadata, and folder path */ function listAgents() { const records = []; const seenIds = new Set(); const visit = (baseDir, relativePath, collective) => { if (!fs_1.default.existsSync(baseDir)) return; const entries = fs_1.default.readdirSync(baseDir, { withFileTypes: true }); entries.forEach((entry) => { const entryPath = path_1.default.join(baseDir, entry.name); if (entry.isDirectory()) { visit(entryPath, relativePath ? path_1.default.join(relativePath, entry.name) : entry.name, collective); 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 relativeId = rawId.replace(/\.md$/i, '').split(path_1.default.sep).join('/'); const canonicalId = collective && collective !== 'root' ? `${collective}/${relativeId}`.replace(/\\/g, '/') : relativeId.replace(/\\/g, '/'); // Skip if already seen (local agents override npm package agents) if (seenIds.has(canonicalId)) return; seenIds.add(canonicalId); const content = fs_1.default.readFileSync(entryPath, 'utf8'); const { meta } = extractFrontMatter(content); const metaObj = meta || {}; if (metaObj.hidden === true || metaObj.disabled === true) return; // Transform display path (strip template/category folders) const { displayId, displayFolder } = (0, display_transform_1.transformDisplayPath)(canonicalId); const label = (metaObj.name || displayId.split('/').pop() || displayId).trim(); records.push({ id: canonicalId, displayId, label, meta: metaObj, folder: displayFolder, collective: collective ?? null }); }); }; // Visit local agents first (user project) const collectives = getLocalCollectives().filter(info => Boolean(info.agentsDir)); collectives.forEach((info) => visit(info.agentsDir, null, info.collective)); return records; } function listCollectives() { return getLocalCollectives(); } /** * Resolves agent identifier to canonical agent path. * * Tries multiple resolution strategies: direct path match, exact ID match, * label match, legacy prefix handling (genie-, template-), and special cases (forge-master). * * @param {string} input - Agent identifier (e.g., "plan", "core/tracer", "forge-master") * @returns {string} - Canonical agent ID (path without .md extension) * @throws {Error} - If agent cannot be found * * @example * resolveAgentIdentifier('plan') // Returns: 'plan' * resolveAgentIdentifier('implementor') // Returns: 'core/implementor' * resolveAgentIdentifier('forge-master') // Returns: 'forge' (legacy alias) */ function resolveAgentIdentifier(input) { const trimmed = (input || '').trim(); if (!trimmed) { throw new Error('Agent id is required'); } const normalized = trimmed.replace(/\.md$/i, ''); const normalizedLower = normalized.toLowerCase(); const directCandidates = [normalized, normalizedLower]; for (const candidate of directCandidates) { const resolved = resolveAgentPath(candidate); if (resolved) return resolved.replace(/\\/g, '/'); } const agents = listAgents(); const byExactId = agents.find((agent) => agent.id.toLowerCase() === normalizedLower); if (byExactId) return byExactId.id; const byLabel = agents.find((agent) => agent.label.toLowerCase() === normalizedLower); if (byLabel) return byLabel.id; const legacy = normalizedLower.replace(/^genie-/, '').replace(/^template-/, ''); const legacyCandidates = [legacy, `core/${legacy}`]; for (const candidate of legacyCandidates) { if (agentExists(candidate)) return candidate; } if (normalizedLower === 'forge-master' && agentExists('forge')) return 'forge'; throw new Error(`❌ Agent '${input}' not found. Try 'genie list agents' to see available ids.`); } /** * Checks if an agent exists at the given path. * * @param {string} id - Agent identifier (without .md extension) * @returns {boolean} - True if agent file exists */ function agentExists(id) { if (!id) return false; return resolveAgentPath(id) !== null; } /** * Loads agent specification from markdown file with frontmatter metadata. * Checks local collectives first, then npm package location. * * @param {string} name - Agent name/path (with or without .md extension) * @returns {AgentSpec} - Object containing metadata and instructions * @throws {Error} - If agent file doesn't exist * * @example * const spec = loadAgentSpec('plan'); * // Returns: { meta: { name: 'plan', ... }, instructions: '...' } */ function loadAgentSpec(name) { const base = name.endsWith('.md') ? name.slice(0, -3) : name; let normalized = base; try { normalized = resolveAgentIdentifier(base); } catch (_) { if (!agentExists(normalized)) { throw new Error(`❌ Agent '${name}' not found`); } } const collectives = getLocalCollectives().filter(info => Boolean(info.agentsDir)); const fileInfo = findAgentFile(normalized, collectives); if (!fileInfo) { throw new Error(`❌ Agent '${name}' not found`); } const content = fs_1.default.readFileSync(fileInfo.path, 'utf8'); const { meta, body } = extractFrontMatter(content); return { meta, instructions: body.replace(/^(\r?\n)+/, ''), filePath: fileInfo.path }; } /** * Extracts YAML frontmatter and body content from markdown source. * * Parses frontmatter delimited by `---` markers at the start of the file. * Falls back to empty metadata if YAML module unavailable or parsing fails. * * @param {string} source - Markdown source content * @returns {{ meta?: Record<string, any>; body: string }} - Parsed metadata and remaining body * * @example * const { meta, body } = extractFrontMatter('---\nname: plan\n---\n# Content'); * // Returns: { meta: { name: 'plan' }, body: '# Content' } */ function extractFrontMatter(source) { if (!source.startsWith('---')) { return { meta: {}, body: source }; } const end = source.indexOf('\n---', 3); if (end === -1) { return { meta: {}, body: source }; } const raw = source.slice(3, end).trim(); const body = source.slice(end + 4); if (!YAML) { (0, config_1.recordStartupWarning)('[genie] YAML module unavailable; falling back to basic front matter parsing.'); return { meta: fallbackParseFrontMatter(raw), body }; } try { const parsed = YAML.parse(raw) || {}; return { meta: parsed, body }; } catch { return { meta: fallbackParseFrontMatter(raw), body }; } }