/** * Agent discovery. Adapted from pi's official subagent example. * Loads markdown agent definitions with YAML frontmatter from * ~/.pi/agent/agents/*.md (user, always) * /.pi/agents/*.md (project, only with scope project|both) */ import * as fs from "node:fs"; import * as path from "node:path"; import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter, } from "@earendil-works/pi-coding-agent"; export type AgentScope = "user" | "project" | "both"; export interface AgentConfig { name: string; description: string; tools?: string[]; model?: string; systemPrompt: string; source: "user" | "project"; filePath: string; } export interface AgentDiscoveryResult { agents: AgentConfig[]; projectAgentsDir: string | null; } function loadAgentsFromDir( dir: string, source: "user" | "project", ): AgentConfig[] { const agents: AgentConfig[] = []; if (!fs.existsSync(dir)) return agents; let entries: fs.Dirent[]; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return agents; } for (const entry of entries) { if (!entry.name.endsWith(".md")) continue; if (!entry.isFile() && !entry.isSymbolicLink()) continue; const filePath = path.join(dir, entry.name); let content: string; try { content = fs.readFileSync(filePath, "utf-8"); } catch { continue; } let parsed: ReturnType>>; try { parsed = parseFrontmatter>(content); } catch { // One malformed definition must not prevent every other named agent // from being discovered. continue; } const { frontmatter, body } = parsed; if ( typeof frontmatter.name !== "string" || typeof frontmatter.description !== "string" ) { continue; } const tools = Array.isArray(frontmatter.tools) ? frontmatter.tools.filter((tool): tool is string => typeof tool === "string") : typeof frontmatter.tools === "string" ? frontmatter.tools.split(",") : []; const normalizedTools = tools.map((tool) => tool.trim()).filter(Boolean); agents.push({ name: frontmatter.name, description: frontmatter.description, tools: normalizedTools.length > 0 ? normalizedTools : undefined, model: typeof frontmatter.model === "string" ? frontmatter.model : undefined, systemPrompt: body, source, filePath, }); } return agents; } function isDirectory(p: string): boolean { try { return fs.statSync(p).isDirectory(); } catch { return false; } } function findNearestProjectAgentsDir(cwd: string): string | null { let currentDir = cwd; while (true) { const candidate = path.join(currentDir, CONFIG_DIR_NAME, "agents"); if (isDirectory(candidate)) return candidate; const parentDir = path.dirname(currentDir); if (parentDir === currentDir) return null; currentDir = parentDir; } } export function discoverAgents( cwd: string, scope: AgentScope, ): AgentDiscoveryResult { const userDir = path.join(getAgentDir(), "agents"); const projectAgentsDir = findNearestProjectAgentsDir(cwd); const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user"); const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project"); const agentMap = new Map(); if (scope === "both") { for (const a of userAgents) agentMap.set(a.name, a); for (const a of projectAgents) agentMap.set(a.name, a); } else if (scope === "user") { for (const a of userAgents) agentMap.set(a.name, a); } else { for (const a of projectAgents) agentMap.set(a.name, a); } return { agents: Array.from(agentMap.values()), projectAgentsDir }; }