import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"; import { missingAuthGuidance } from "./auth.ts"; import { getPiWebConfigPaths, loadPiWebConfig, PiWebConfigError, savePiWebConfig, type ConfigScope } from "./config.ts"; import type { OpenAISearchProvider, PiWebConfigFile, SearchContextSize, SearchPipeline, WebSearchMode } from "./types.ts"; const MANUAL_MODEL = "Enter a model ID manually"; async function showConfiguration(ctx: ExtensionCommandContext): Promise { try { const config = await loadPiWebConfig({ cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted(), }); const visible = { ...config, ...(config.exaApiKey ? { exaApiKey: "[configured]" } : {}) }; ctx.ui.notify(`pi-web config (${config.source})\n${JSON.stringify(visible, null, 2)}`, "info"); } catch (error) { ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning"); } } async function chooseInstalledModel( ctx: ExtensionCommandContext, provider: string, title: string, ): Promise { const installedModels = ctx.modelRegistry .getAll() .filter((model) => model.provider === provider) .map((model) => model.id); const modelChoice = await ctx.ui.select(title, [...installedModels, MANUAL_MODEL]); if (!modelChoice) return undefined; const model = modelChoice === MANUAL_MODEL ? await ctx.ui.input("Exact model ID", "The model must already be installed in Pi") : modelChoice; if (!model?.trim()) return undefined; if (!ctx.modelRegistry.find(provider, model.trim())) { ctx.ui.notify(`Model ${provider}/${model.trim()} is not installed in this Pi. Add it first, then rerun /web-config.`, "error"); return undefined; } return model.trim(); } async function configureInteractively(ctx: ExtensionCommandContext): Promise { if (!ctx.hasUI) { const paths = getPiWebConfigPaths(ctx.cwd); throw new Error(`Interactive configuration is unavailable in ${ctx.mode} mode. Create ${paths.global} manually.`); } const pipeline = await ctx.ui.select("Search pipeline", [ "OpenAI direct (no summary)", "OpenAI + summary model", "Exa API (direct results)", ]); if (!pipeline) return; let config: PiWebConfigFile; let contextChoice = "provider default"; if (pipeline === "Exa API (direct results)") { const apiKey = await ctx.ui.input("Exa API key", "Stored in pi-web.json with file mode 0600"); if (!apiKey?.trim()) return; config = { provider: "exa", exa_api_key: apiKey.trim(), mode: "live" }; } else { const provider = (await ctx.ui.select("Standalone search provider", [ "openai-codex", "openai", ])) as OpenAISearchProvider | undefined; if (!provider) return; const model = await chooseInstalledModel(ctx, provider, "Standalone search model"); if (!model) return; config = { provider, model }; if (pipeline === "OpenAI + summary model") { const summaryProviders = [...new Set(ctx.modelRegistry.getAll().map((candidate) => candidate.provider))].sort(); if (summaryProviders.length === 0) { ctx.ui.notify("No installed model is available for search-result summarization.", "error"); return; } const summaryProvider = await ctx.ui.select("Summary model provider", summaryProviders); if (!summaryProvider) return; const summaryModel = await chooseInstalledModel(ctx, summaryProvider, "Per-result summary model (no Agent)"); if (!summaryModel) return; config.summary_provider = summaryProvider; config.summary_model = summaryModel; } const mode = (await ctx.ui.select("Web access mode", ["cached", "indexed", "live"])) as WebSearchMode | undefined; if (!mode) return; config.mode = mode; const selectedContext = await ctx.ui.select("Search context size", ["provider default", "low", "medium", "high"]); if (!selectedContext) return; contextChoice = selectedContext; } const domainsText = await ctx.ui.input( "Allowed domains (optional, comma-separated)", "example.com, docs.example.com", ); if (domainsText === undefined) return; if (contextChoice !== "provider default") config.search_context_size = contextChoice as SearchContextSize; const allowedDomains = domainsText.split(",").map((domain) => domain.trim()).filter(Boolean); if (allowedDomains.length > 0) config.allowed_domains = allowedDomains; const scope = (await ctx.ui.select("Save pi-web configuration", ["global", "project"])) as ConfigScope | undefined; if (!scope) return; if (scope === "project" && !ctx.isProjectTrusted()) { ctx.ui.notify("Project configuration requires a trusted project. Use /trust and restart Pi, or save globally.", "error"); return; } try { const path = await savePiWebConfig(scope, ctx.cwd, config); ctx.ui.notify(`Saved pi-web configuration to ${path}`, "info"); if (config.provider !== "exa") { const searchAuth = ctx.modelRegistry.getProviderAuthStatus(config.provider as OpenAISearchProvider); if (!searchAuth.configured) { ctx.ui.notify(`Search authentication is missing. ${missingAuthGuidance(config.provider as OpenAISearchProvider)}`, "warning"); } if (config.summary_provider) { const summaryAuth = ctx.modelRegistry.getProviderAuthStatus(config.summary_provider); if (!summaryAuth.configured && config.summary_provider !== config.provider) { ctx.ui.notify(`Summary-model authentication for ${config.summary_provider} is missing. Configure it with Pi before searching.`, "warning"); } } } return config.provider === "exa" ? "exa" : config.summary_provider ? "openai-summary" : "openai"; } catch (error) { ctx.ui.notify(error instanceof Error ? error.message : String(error), "error"); } } export function registerWebConfigCommand( pi: ExtensionAPI, onConfigured?: (pipeline: SearchPipeline) => void, ): void { pi.registerCommand("web-config", { description: "Configure OpenAI direct, OpenAI + summary, or Exa direct search", getArgumentCompletions(prefix) { return ["show", "paths"] .filter((value) => value.startsWith(prefix.trim())) .map((value) => ({ value, label: value })); }, handler: async (args, ctx) => { const command = args.trim(); if (command === "show") { await showConfiguration(ctx); return; } if (command === "paths") { const paths = getPiWebConfigPaths(ctx.cwd); ctx.ui.notify(`Global: ${paths.global}\nProject: ${paths.project}`, "info"); return; } if (command) { ctx.ui.notify("Usage: /web-config, /web-config show, or /web-config paths", "warning"); return; } try { const pipeline = await configureInteractively(ctx); if (pipeline) onConfigured?.(pipeline); } catch (error) { const level = error instanceof PiWebConfigError ? "warning" : "error"; ctx.ui.notify(error instanceof Error ? error.message : String(error), level); } }, }); }