/** * Company Context Extension * * WHAT: On session start, loads config/company.json (the single source of * truth). If `remoteConfigUrl` is set, tries to fetch a fresh config * from there with a short timeout and falls back silently to the local * file on any error. Renders a short corporate context from * `company-context.md` and injects it into the system prompt. Also * registers the `/kit-config` command for debugging effective values. * * CONFIGURE: Edit config/company.json (gitHost, trackerUrl, docsUrl, repoMap, * rules, remoteConfigUrl). Edit the wording in company-context.md — * non-programmers can safely change text between the {{placeholders}}. * * DISABLE: Remove/rename this file from the package `extensions/` folder, * or exclude it via the package manifest / settings. */ import * as path from "node:path"; import { fileURLToPath } from "node:url"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { readLocalConfig, fetchRemoteConfig, loadTemplate, renderContext, DEFAULT_CONFIG, type CompanyConfig, } from "./lib/company-config.ts"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const CONFIG_PATH = path.join(HERE, "..", "config", "company.json"); const TEMPLATE_PATH = path.join(HERE, "company-context.md"); export default function companyContextExtension(pi: ExtensionAPI) { const template = loadTemplate(TEMPLATE_PATH); // Effective config and where it came from. Refreshed on every session start. let effective: CompanyConfig = { ...DEFAULT_CONFIG }; let source: "local" | "remote" = "local"; // Rendered once per session; injected by before_agent_start. let rendered: string | null = null; pi.on("session_start", async (_event, ctx) => { effective = readLocalConfig(CONFIG_PATH); source = "local"; if (effective.remoteConfigUrl) { const remote = await fetchRemoteConfig(effective.remoteConfigUrl); if (remote) { // Remote wins, but keep the local remoteConfigUrl so a bad remote // value can't orphan future refreshes. effective = { ...remote, remoteConfigUrl: effective.remoteConfigUrl }; source = "remote"; } } const { text, truncated } = renderContext(template, effective); rendered = text; if (ctx.hasUI) { if (truncated) { ctx.ui.notify( `pi-kit: корпоративный контекст обрезан до ${text.split("\n").length} строк (лимит) — часть правил не попала в контекст.`, "warning", ); } else { ctx.ui.notify(`pi-kit: корпоративный контекст загружен (источник: ${source})`, "info"); } } }); pi.on("before_agent_start", async (event) => { // session_start may not have fired (rare); render on demand as a fallback. const context = rendered ?? renderContext(template, effective).text; if (!context) return undefined; return { systemPrompt: `${event.systemPrompt}\n\n${context}` }; }); pi.registerCommand("kit-config", { description: "Показать действующие корпоративные значения pi-kit и их источник", handler: async (_args, ctx) => { const origin = source === "remote" ? `remote (${effective.remoteConfigUrl})` : `local (${CONFIG_PATH})`; const lines = [ `Источник: ${origin}`, "", `gitHost: ${effective.gitHost}`, `trackerUrl: ${effective.trackerUrl}`, `docsUrl: ${effective.docsUrl}`, `repoMap: ${effective.repoMap}`, `remoteConfigUrl: ${effective.remoteConfigUrl ?? "(не задан)"}`, "rules:", ...(effective.rules.length > 0 ? effective.rules.map((r) => ` - ${r}`) : [" (нет)"]), ]; if (ctx.hasUI) { await ctx.ui.select("Действующая конфигурация pi-kit", lines); } else { ctx.ui.notify(lines.join("\n"), "info"); } }, }); }