import { existsSync, readFileSync, readdirSync } from "node:fs"; import { join } from "node:path"; import { DefaultResourceLoader, type SettingsManager } from "@mariozechner/pi-coding-agent"; import { getProjectRoot } from "./config.js"; type PiResourceLoaderArgs = { cwd: string; agentDir?: string; settingsManager: SettingsManager; }; export function getPiResourcesDir(): string { return join(getProjectRoot(), "pi-resources"); } export function buildPiResourceLoaderOptions( args: PiResourceLoaderArgs ): ConstructorParameters[0] { const piResourcesDir = getPiResourcesDir(); const repoSystemPromptPath = join(piResourcesDir, "SYSTEM.md"); const repoAppendSystemPromptPath = join(piResourcesDir, "APPEND_SYSTEM.md"); const localSystemPromptPath = join(args.cwd, ".pi", "SYSTEM.md"); const localAppendSystemPromptPath = join(args.cwd, ".pi", "APPEND_SYSTEM.md"); const systemPromptSource = pickFirstExistingPath([localSystemPromptPath, repoSystemPromptPath]); const appendSystemPromptSource = pickFirstExistingPath([ localAppendSystemPromptPath, repoAppendSystemPromptPath ]); return { cwd: args.cwd, agentDir: args.agentDir, settingsManager: args.settingsManager, additionalExtensionPaths: discoverExtensionEntries(join(piResourcesDir, "extensions")), additionalSkillPaths: existingPaths([join(piResourcesDir, "skills")]), additionalPromptTemplatePaths: existingPaths([join(piResourcesDir, "prompts")]), // Keep the bot runtime independent from repo/workspace AGENTS.md files. agentsFilesOverride: () => ({ agentsFiles: [] }), // Make source precedence explicit instead of expressing it indirectly through // override callbacks: workspace/.pi wins, then repo-managed resources, then the // loader's own defaults when neither exists. systemPrompt: systemPromptSource, appendSystemPrompt: appendSystemPromptSource }; } export function createPiResourceLoader(args: PiResourceLoaderArgs): DefaultResourceLoader { return new DefaultResourceLoader(buildPiResourceLoaderOptions(args)); } function existingPaths(paths: string[]): string[] { return paths.filter((path) => existsSync(path)); } function pickFirstExistingPath(paths: string[]): string | undefined { return paths.find((path) => existsSync(path)); } function discoverExtensionEntries(dir: string): string[] { if (!existsSync(dir)) { return []; } const discovered = new Set(); for (const entry of readdirSync(dir, { withFileTypes: true })) { if (entry.name.startsWith(".")) { continue; } const entryPath = join(dir, entry.name); if (entry.isFile() && isExtensionModule(entry.name)) { discovered.add(entryPath); continue; } if (!entry.isDirectory()) { continue; } for (const extensionEntry of resolveExtensionEntries(entryPath)) { discovered.add(extensionEntry); } } return Array.from(discovered); } function resolveExtensionEntries(dir: string): string[] { const packageJsonPath = join(dir, "package.json"); if (existsSync(packageJsonPath)) { const manifest = readPiManifest(packageJsonPath); if (manifest?.extensions?.length) { return manifest.extensions .map((extensionPath) => join(dir, extensionPath)) .filter((extensionPath) => existsSync(extensionPath)); } } const indexTs = join(dir, "index.ts"); if (existsSync(indexTs)) { return [indexTs]; } const indexJs = join(dir, "index.js"); if (existsSync(indexJs)) { return [indexJs]; } return []; } function readPiManifest(packageJsonPath: string): { extensions?: string[] } | undefined { try { const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { pi?: { extensions?: string[] }; }; return pkg.pi; } catch { return undefined; } } function isExtensionModule(fileName: string): boolean { return fileName.endsWith(".ts") || fileName.endsWith(".js"); }