/** * Kit CLI Extension * * WHAT: developer-experience commands for the corporate kit. * /kit-doctor — environment health check (node, kit version, config, env vars, * env.sh, loaded commands/skills). Read-only, offline-safe. * /kit-help — catalog of everything this package provides (commands, skills, * guardrails). * * CONFIGURE: nothing to configure. Values are read from config/company.json and * the environment. (`/kit-config` — in company-context.ts — shows the * effective corporate config.) * * DISABLE: remove/rename this file from the package `extensions/` folder. */ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const PKG_ROOT = path.join(HERE, ".."); const CONFIG_PATH = path.join(PKG_ROOT, "config", "company.json"); const ENV_FILE = path.join(os.homedir(), ".config", "pi-kit", "env.sh"); const PROVIDER_KEYS = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY"]; // Guardrail extensions don't register commands, so list them statically here. const GUARDRAILS: Array<[string, string]> = [ ["protected-paths", "блок записи в секреты/ключи/.git/прод-конфиги (по пути)"], ["permission-gate", "подтверждение опасных bash (rm -rf, force push, release-*/v* деплой, sudo …)"], ["secret-scanner", "блок записи контента, похожего на секрет"], ["commit-guard", "Conventional Commits + скан секретов в сообщении коммита"], ["llm-redaction", "редакция секретов из payload перед отправкой в LLM"], ["audit-log", "локальный аудит (~/.config/pi-kit/audit.jsonl)"], ]; function readVersion(): string { try { return JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf8")).version ?? "?"; } catch { return "?"; } } function readConfig(): Record { try { return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8")); } catch { return {}; } } /** Show lines in a dismissable selector (UI) or as a notification (no UI). */ async function show(ctx: any, title: string, lines: string[]): Promise { if (ctx.hasUI) await ctx.ui.select(title, lines); else ctx.ui.notify(`${title}\n${lines.join("\n")}`, "info"); } export default function kitCliExtension(pi: ExtensionAPI) { pi.registerCommand("kit-doctor", { description: "Проверка окружения pi-kit (node, версия, конфиг, env, токены)", handler: async (_args, ctx) => { const ok = (s: string) => `✓ ${s}`; const warn = (s: string) => `⚠ ${s}`; const cfg = readConfig(); const lines: string[] = []; lines.push(ok(`Node.js ${process.version}`)); lines.push(ok(`pi-kit v${readVersion()}${process.env.PI_KIT_CHANNEL ? ` (канал: ${process.env.PI_KIT_CHANNEL})` : ""}`)); // Config const cfgOk = typeof cfg.gitHost === "string" && cfg.gitHost !== "TODO"; lines.push((cfgOk ? ok : warn)(`config: gitHost=${cfg.gitHost ?? "?"}, tracker=${cfg.trackerUrl ?? "?"}, docs=${cfg.docsUrl ?? "?"}`)); if (cfg.remoteConfigUrl) lines.push(ok(`remoteConfigUrl задан`)); // Env vars (presence only, never values) for (const v of ["TRACKER_URL", "JIRA_TOKEN", "GITLAB_TOKEN"]) { lines.push((process.env[v] ? ok : warn)(`${v} ${process.env[v] ? "задан" : "не задан"}`)); } const provider = PROVIDER_KEYS.find((k) => process.env[k]); lines.push((provider ? ok : warn)(provider ? `LLM-ключ: ${provider}` : "LLM-ключ провайдера не найден")); // env.sh lines.push((fs.existsSync(ENV_FILE) ? ok : warn)(`env.sh ${fs.existsSync(ENV_FILE) ? "есть" : "нет"} (${ENV_FILE})`)); // Loaded commands/skills const cmds = pi.getCommands(); const skills = cmds.filter((c) => c.source === "skill").length; lines.push(ok(`команд загружено: ${cmds.length} (скилов: ${skills})`)); const warns = lines.filter((l) => l.startsWith("⚠")).length; lines.push("", warns === 0 ? "Итог: всё ок." : `Итог: предупреждений — ${warns}.`); await show(ctx, "pi-kit doctor", lines); }, }); pi.registerCommand("kit-help", { description: "Каталог возможностей pi-kit (команды, скилы, guardrails)", handler: async (_args, ctx) => { const cmds = pi.getCommands(); const mine = cmds.filter((c) => c.sourceInfo?.path?.startsWith(PKG_ROOT)); const bySource = (src: string) => mine .filter((c) => c.source === src) .sort((a, b) => a.name.localeCompare(b.name)) .map((c) => ` /${c.name}${c.description ? ` — ${c.description}` : ""}`); const lines: string[] = []; lines.push("— Команды —", ...bySource("extension"), ...bySource("prompt")); lines.push("", "— Скилы —", ...bySource("skill")); lines.push("", "— Guardrails (расширения) —", ...GUARDRAILS.map(([n, d]) => ` ${n} — ${d}`)); await show(ctx, "pi-kit help", lines); }, }); }