// horst — per-host environment data and tool defaults. import os from "node:os"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { getHostSection, isHostSnapshotCached } from "./host.ts"; import { EMPTY_HORST_SETTINGS, type HorstSettings, parseHorst, resolveHorstTools, } from "./settings.ts"; import { closeDebug, dbg, span } from "./src/debug.ts"; import { resolveSetting, type SettingDeclaration, } from "./src/pi-ext-settings.ts"; // Precedence: trusted project, user, empty; objects deep-merge, project wins. const HORST_SETTING: SettingDeclaration = { key: "horst", parse: parseHorst, default: EMPTY_HORST_SETTINGS, }; export default function horstExtension(pi: ExtensionAPI) { let preferredShell: string | undefined; pi.on("session_start", async (_event, ctx) => { dbg?.("session.start", { mode: ctx.mode }); const setting = resolveSetting(pi, ctx, HORST_SETTING); if (!setting.ok) ctx.ui.notify(`horst: ${setting.error}; settings ignored`, "warning"); const settings = setting.ok ? setting.value : EMPTY_HORST_SETTINGS; preferredShell = settings.preferredShell; const apply = span?.("tools.apply"); const delta = resolveHorstTools(settings, process.platform, os.hostname()); if (!delta) { apply?.("finish", { outcome: "no-match" }); return; } const known = new Set(pi.getAllTools().map((tool) => tool.name)); const unknown = [...(delta.add ?? []), ...(delta.remove ?? [])].filter( (name) => !known.has(name), ); if (unknown.length > 0) ctx.ui.notify(`horst: unknown tools: ${unknown.join(", ")}`, "warning"); const active = pi.getActiveTools(); const next = active.filter((name) => !delta.remove?.includes(name)); for (const name of delta.add ?? []) { if (known.has(name) && !next.includes(name)) next.push(name); } const addedCount = next.filter((name) => !active.includes(name)).length; const removedCount = active.filter((name) => !next.includes(name)).length; pi.setActiveTools(next); apply?.("finish", { outcome: addedCount > 0 || removedCount > 0 ? "updated" : "no-change", addedCount, removedCount, unknownCount: unknown.length, }); }); pi.on("before_agent_start", async (event) => { const end = span?.("host.section", { cached: isHostSnapshotCached() }); try { event.systemPromptOptions.sections.horst = await getHostSection(preferredShell); end?.(); } catch (error) { end?.("error", { type: "host" }); throw error; } }); pi.on("session_shutdown", () => { dbg?.("session.shutdown"); closeDebug(); }); }