import { readFileSync, existsSync, readdirSync } from 'node:fs' import { join, extname } from 'node:path' import { parse as parseYaml } from 'yaml' import type { AgentDefinition, SubAgentType } from './types' import { miphamHome } from '../core/paths.ts' import { MIPHAM_DIR } from '../shared/constants.ts' interface FrontmatterResult { data: Record content: string } function parseFrontmatter(raw: string): FrontmatterResult { // Strip a leading UTF-8 BOM — otherwise `^---` never matches and a // BOM-prefixed file is silently treated as body text (effectively ignored). const src = raw.replace(/^\uFEFF/, '') const match = src.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/) if (!match) { return { data: {}, content: src } } return { data: parseYaml(match[1] || '') as Record, content: (match[2] || '').trim(), } } const BUILTIN_SYSTEM_PROMPTS: Record = { general: 'You are a focused sub-agent. Complete the assigned task thoroughly and return results.', explore: 'You are an exploration sub-agent. Search, read, and analyze code. Return structured findings with file paths and line numbers.', plan: 'You are a planning sub-agent. Design implementation approaches. Return a step-by-step plan with files to modify.', 'code-review': 'You are a code review sub-agent. Find bugs, security issues, and code quality problems. Return findings by severity.', } const BUILTIN_DESCRIPTIONS: Record = { general: 'General-purpose agent for complex multi-step tasks.', explore: 'Read-only search agent for broad fan-out searches across files.', plan: 'Software architect agent for designing implementation plans.', 'code-review': 'Code review agent for finding bugs and quality issues.', } export class AgentRegistry { private agents = new Map() /** Load agents from a directory. Later loads override earlier ones for same name. */ loadDirectory(dir: string, source: 'project' | 'user'): void { if (!existsSync(dir)) return let entries: string[] = [] try { entries = readdirSync(dir) } catch { // Permission errors, missing dir, etc. return } for (const entry of entries) { if (extname(entry) !== '.md') continue const fullPath = join(dir, entry) try { const raw = readFileSync(fullPath, 'utf-8') const { data, content } = parseFrontmatter(raw) const name = (data.name as string) || entry.replace(/\.md$/, '') const def: AgentDefinition = { name, description: (data.description as string) || '', systemPrompt: content, tools: data.tools as string | undefined, disallowedTools: data.disallowedTools as string | undefined, model: (data.model as string) || 'inherit', permissionMode: (data.permissionMode as string) || 'inherit', maxTurns: data.maxTurns as number | undefined, skills: data.skills ? String(data.skills) .split(',') .map((s) => s.trim()) : undefined, background: typeof data.background === 'boolean' ? data.background : undefined, memory: data.memory as 'user' | 'project' | 'local' | undefined, source, filePath: fullPath, } this.agents.set(name, def) } catch { // Skip unparseable files } } } /** Load project-level agents from .mipham/agents/ */ loadProjectAgents(cwd: string): void { this.loadDirectory(join(cwd, MIPHAM_DIR, 'agents'), 'project') } /** Load user-level agents from ~/.mipham/agents/ */ loadUserAgents(): void { this.loadDirectory(miphamHome('agents'), 'user') } /** Get a custom agent by name. Returns undefined for builtins. */ get(name: string): AgentDefinition | undefined { return this.agents.get(name) } /** List all custom agents. */ list(): AgentDefinition[] { return Array.from(this.agents.values()) } /** * Resolve an agent name to its definition. * Priority: custom (project) > custom (user) > builtin. * Builtins are always available and never return undefined. */ resolve(name: string): AgentDefinition | undefined { const custom = this.agents.get(name) if (custom) return custom // Check if it's a builtin type const builtinType = name as SubAgentType if (BUILTIN_SYSTEM_PROMPTS[builtinType]) { return { name: builtinType, description: BUILTIN_DESCRIPTIONS[builtinType], systemPrompt: BUILTIN_SYSTEM_PROMPTS[builtinType], model: 'inherit', permissionMode: 'inherit', source: 'builtin', } } return undefined } }