import * as fs from "node:fs"; import * as path from "node:path"; import { DefaultPackageManager, type ExtensionContext, getAgentDir, parseFrontmatter, SettingsManager } from "@earendil-works/pi-coding-agent"; export interface AgentConfig { name: string; description: string; tools?: string[]; model?: string; thinkingLevel?: string; /** undefined = no extensions; non-empty = exactly these extensions */ extensions?: string[]; /** undefined = no skills; non-empty = exactly these skill names */ skills?: string[]; systemPrompt: string; source: "user" | "project"; filePath: string; } export function parseCommaList(value: unknown): string[] | undefined { if (typeof value !== "string") return undefined; const parts = value .split(",") .map((s) => s.trim()) .filter(Boolean); return parts.length > 0 ? parts : undefined; } function substituteEnvVars(paths: string[] | undefined): string[] | undefined { if (!paths) return paths; return paths.map((p) => p.replace(/\$\{(\w+)\}/g, (match, name) => process.env[name] ?? match)); } 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; } const { frontmatter, body } = parseFrontmatter>(content); if (!frontmatter.name || !frontmatter.description) { continue; } agents.push({ name: frontmatter.name as string, description: frontmatter.description as string, tools: parseCommaList(frontmatter.tools), model: typeof frontmatter.model === "string" ? frontmatter.model : undefined, thinkingLevel: typeof frontmatter.thinkingLevel === "string" ? frontmatter.thinkingLevel : undefined, extensions: substituteEnvVars(parseCommaList(frontmatter.extensions)), skills: parseCommaList(frontmatter.skills), systemPrompt: body, source, filePath, }); } return agents; } function isDirectory(p: string): boolean { try { return fs.statSync(p).isDirectory(); } catch { return false; } } function getPackageAgentsDirs( settings: SettingsManager, packages: DefaultPackageManager, scope: "user" | "project", ): string[] { const pkgList = scope === "user" ? settings.getGlobalSettings().packages ?? [] : settings.getProjectSettings().packages ?? []; const dirs: string[] = []; for (const pkg of pkgList) { const source = typeof pkg === "string" ? pkg : pkg.source; const root = packages.getInstalledPath(source, scope); if (root) { const agentsDir = path.join(root, "agents"); if (isDirectory(agentsDir)) dirs.push(agentsDir); } } return dirs; } function getExtensionAgentsDirs(baseDir: string): string[] { const extDir = path.join(baseDir, "extensions"); if (!isDirectory(extDir)) return []; const dirs: string[] = []; for (const entry of fs.readdirSync(extDir, { withFileTypes: true })) { if (entry.isDirectory() || entry.isSymbolicLink()) { const agentsDir = path.join(extDir, entry.name, "agents"); if (isDirectory(agentsDir)) dirs.push(agentsDir); } } return dirs; } export function discoverAgents(ctx: ExtensionContext): AgentConfig[] { const agentDir = getAgentDir(); const settings = SettingsManager.create(ctx.cwd, agentDir); const packages = new DefaultPackageManager({ cwd: ctx.cwd, agentDir, settingsManager: settings }); const agentMap = new Map(); // User: lowest → highest precedence for (const dir of [ ...getPackageAgentsDirs(settings, packages, "user"), ...getExtensionAgentsDirs(agentDir), path.join(agentDir, "agents"), ]) for (const agent of loadAgentsFromDir(dir, "user")) agentMap.set(agent.name, agent); // Project: overwrites user; lowest → highest precedence if (ctx.isProjectTrusted()) { for (const dir of [ ...getPackageAgentsDirs(settings, packages, "project"), ...getExtensionAgentsDirs(path.join(ctx.cwd, ".pi")), path.join(ctx.cwd, ".pi", "agents"), ]) for (const agent of loadAgentsFromDir(dir, "project")) agentMap.set(agent.name, agent); } return Array.from(agentMap.values()); } export function formatAgentList( agents: AgentConfig[], maxItems: number, ): { text: string; remaining: number } { if (agents.length === 0) return { text: "none", remaining: 0 }; const listed = agents.slice(0, maxItems); const remaining = agents.length - listed.length; return { text: listed.map((a) => `${a.name} (${a.source}): ${a.description}`).join("; "), remaining, }; }