/** * Pi extension: Kimchi (kimchi-dev) provider for /login → Use a subscription. */ import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { authenticateViaBrowser } from "./browser-auth.js"; import { readKimchiCliApiKey } from "./kimchi-config.js"; import { DEFAULT_ENDPOINT, FALLBACK_MODELS, PROVIDER_ID, chatCompletionsBaseUrl, fetchKimchiModels, toPiModels, } from "./models.js"; const PROVIDER_LABEL = "Kimchi"; // ponytail: fixed UA string; bump when publishing if Kimchi version matters for billing const KIMCHI_USER_AGENT = "kimchi/0.1.50"; let cachedModels = toPiModels(FALLBACK_MODELS); let cachedBaseUrl = chatCompletionsBaseUrl(); async function refreshModelsFromToken(token: string, endpoint?: string): Promise { try { const endpointNorm = endpoint?.trim() || DEFAULT_ENDPOINT; cachedBaseUrl = chatCompletionsBaseUrl(endpointNorm); cachedModels = toPiModels(await fetchKimchiModels(token, endpointNorm)); } catch { cachedModels = toPiModels(FALLBACK_MODELS); cachedBaseUrl = chatCompletionsBaseUrl(endpoint); } } function setKimchiOAuthToken( ctx: Parameters[1]>[1], access: string, ): void { ctx.modelRegistry.authStorage.set(PROVIDER_ID, { type: "oauth", access, refresh: "", expires: Number.MAX_SAFE_INTEGER, }); } async function kimchiOAuthLogin(callbacks: OAuthLoginCallbacks): Promise { callbacks.onProgress?.("Opening browser for Kimchi login…"); const token = await authenticateViaBrowser({ onBrowserUrl: (url) => callbacks.onAuth({ url, instructions: "Log in at app.kimchi.dev to finish." }), signal: callbacks.signal, }); await refreshModelsFromToken(token); return { access: token, refresh: "", expires: Number.MAX_SAFE_INTEGER }; } function providerConfig() { return { name: PROVIDER_LABEL, baseUrl: cachedBaseUrl, apiKey: "$KIMCHI_API_KEY", api: "openai-completions" as const, authHeader: true, headers: { "User-Agent": KIMCHI_USER_AGENT }, models: cachedModels, oauth: { name: PROVIDER_LABEL, login: kimchiOAuthLogin, refreshToken: async (c: OAuthCredentials) => c, getApiKey: (c: OAuthCredentials) => c.access, }, }; } export default function (pi: ExtensionAPI) { pi.registerProvider(PROVIDER_ID, providerConfig()); pi.on("session_start", async (_ev, ctx) => { // Match Kimchi CLI: prefer apiKey from ~/.config/kimchi/config.json (harness token). const cliKey = readKimchiCliApiKey(); if (cliKey) { setKimchiOAuthToken(ctx, cliKey); } const cred = ctx.modelRegistry.authStorage.get(PROVIDER_ID) as | { type: string; access?: string; key?: string } | undefined; const token = cred?.type === "oauth" ? cred.access : cred?.type === "api_key" ? cred.key : undefined; if (token) await refreshModelsFromToken(token); pi.registerProvider(PROVIDER_ID, providerConfig()); }); }