96 lines
2.9 KiB
TypeScript
96 lines
2.9 KiB
TypeScript
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
|
|
export interface SubagentPreset {
|
|
name: string;
|
|
description: string;
|
|
model?: string;
|
|
tools?: string[];
|
|
systemPrompt: string;
|
|
source: "global" | "project";
|
|
filePath: string;
|
|
}
|
|
|
|
export interface SubagentPresetDiscoveryResult {
|
|
presets: SubagentPreset[];
|
|
projectPresetsDir: string | null;
|
|
}
|
|
|
|
function findNearestProjectPresetsDir(cwd: string): string | null {
|
|
let current = cwd;
|
|
while (true) {
|
|
const candidate = join(current, ".pi", "subagents");
|
|
try {
|
|
if (statSync(candidate).isDirectory()) return candidate;
|
|
} catch (_err) {
|
|
// ignore
|
|
}
|
|
const parent = dirname(current);
|
|
if (parent === current) return null;
|
|
current = parent;
|
|
}
|
|
}
|
|
|
|
function loadPresetDir(dir: string, source: "global" | "project"): SubagentPreset[] {
|
|
if (!existsSync(dir)) return [];
|
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
const presets: SubagentPreset[] = [];
|
|
|
|
for (const entry of entries) {
|
|
if (!entry.name.endsWith(".md")) continue;
|
|
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
const filePath = join(dir, entry.name);
|
|
let content: string;
|
|
try {
|
|
content = readFileSync(filePath, "utf8");
|
|
} catch (_err) {
|
|
continue;
|
|
}
|
|
|
|
const parsed = parseFrontmatter<Record<string, unknown>>(content);
|
|
const frontmatter = parsed.frontmatter ?? {};
|
|
const body = parsed.body ?? "";
|
|
|
|
if (typeof frontmatter.name !== "string") continue;
|
|
if (typeof frontmatter.description !== "string") continue;
|
|
|
|
const tools = typeof frontmatter.tools === "string"
|
|
? frontmatter.tools.split(",").map((t) => t.trim()).filter(Boolean)
|
|
: undefined;
|
|
|
|
presets.push({
|
|
name: frontmatter.name,
|
|
description: frontmatter.description,
|
|
model: typeof frontmatter.model === "string" ? frontmatter.model : undefined,
|
|
tools: tools && tools.length > 0 ? tools : undefined,
|
|
systemPrompt: body.trim(),
|
|
source,
|
|
filePath,
|
|
});
|
|
}
|
|
|
|
return presets;
|
|
}
|
|
|
|
export function discoverSubagentPresets(cwd: string, options: { homeDir?: string } = {}): SubagentPresetDiscoveryResult {
|
|
const globalAgentDir = options.homeDir ? join(options.homeDir, ".pi", "agent") : getAgentDir();
|
|
const globalDir = join(globalAgentDir, "subagents");
|
|
const projectPresetsDir = findNearestProjectPresetsDir(cwd);
|
|
|
|
const map = new Map<string, SubagentPreset>();
|
|
|
|
for (const preset of loadPresetDir(globalDir, "global")) {
|
|
map.set(preset.name, preset);
|
|
}
|
|
|
|
if (projectPresetsDir) {
|
|
for (const preset of loadPresetDir(projectPresetsDir, "project")) {
|
|
// project overrides global by name
|
|
map.set(preset.name, preset);
|
|
}
|
|
}
|
|
|
|
return { presets: Array.from(map.values()), projectPresetsDir };
|
|
}
|