import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"; import { missingAuthGuidance } from "./auth.ts"; import { CodexSearchConfigError, getCodexSearchConfigPaths, loadCodexSearchConfig, saveCodexSearchConfig, type ConfigScope, } from "./config.ts"; import type { CodexSearchProvider, CodexSearchConfigFile, SearchContextSize, WebSearchMode, } from "./types.ts"; const MANUAL_MODEL = "Enter a model ID manually"; async function showConfiguration(ctx: ExtensionCommandContext): Promise { try { const config = await loadCodexSearchConfig({ cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted(), }); const displayConfig = config.provider === "openai-compatible" ? { ...config, apiKey: "[configured]" } : config; ctx.ui.notify(`Pi Codex Search config (${config.source})\n${JSON.stringify(displayConfig, null, 2)}`, "info"); } catch (error) { ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning"); } } async function chooseInstalledModel( ctx: ExtensionCommandContext, provider: "openai-codex", ): Promise { const installedModels = ctx.modelRegistry .getAll() .filter((model) => model.provider === provider) .map((model) => model.id); const modelChoice = await ctx.ui.select("Codex search model", [...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. Add it to Pi, then rerun /codex-search-config.`, "error", ); return undefined; } return model.trim(); } async function configureInteractively(ctx: ExtensionCommandContext): Promise { if (!ctx.hasUI) { const paths = getCodexSearchConfigPaths(ctx.cwd); throw new Error(`Interactive configuration is unavailable in ${ctx.mode} mode. Create ${paths.global} manually.`); } const provider = (await ctx.ui.select("Search provider", [ "openai-compatible", "openai-codex", ])) as CodexSearchProvider | undefined; if (!provider) return false; const config: CodexSearchConfigFile = { provider }; if (provider === "openai-compatible") { const baseUrl = await ctx.ui.input("Gateway base URL", "https://your-sub2api.example/v1"); if (!baseUrl?.trim()) return false; const apiKey = await ctx.ui.input("Gateway API key (input is visible)", "Stored in pi-codex-search.json with file mode 0600"); if (!apiKey?.trim()) return false; const model = await ctx.ui.input("Remote search model ID", "gpt-5.6-luna"); if (!model?.trim()) return false; config.baseUrl = baseUrl.trim().replace(/\/+$/, ""); config.apiKey = apiKey.trim(); config.model = model.trim(); } else { const model = await chooseInstalledModel(ctx, provider); if (!model) return false; config.model = model; } const mode = (await ctx.ui.select("Web access mode", ["live", "cached", "indexed"])) as WebSearchMode | undefined; if (!mode) return false; config.mode = mode; const contextChoice = await ctx.ui.select("Search context size", ["provider default", "low", "medium", "high"]); if (!contextChoice) return false; if (contextChoice !== "provider default") config.search_context_size = contextChoice as SearchContextSize; const domainsText = await ctx.ui.input("Allowed domains (optional, comma-separated)", "example.com, docs.example.com"); if (domainsText === undefined) return false; 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 Codex Search configuration", ["global", "project"])) as ConfigScope | undefined; if (!scope) return false; if (scope === "project" && !ctx.isProjectTrusted()) { ctx.ui.notify("Project configuration requires a trusted project. Use /trust and restart Pi, or save globally.", "error"); return false; } const path = await saveCodexSearchConfig(scope, ctx.cwd, config); ctx.ui.notify(`Saved Pi Codex Search configuration to ${path}`, "info"); if (provider === "openai-codex" && !ctx.modelRegistry.getProviderAuthStatus(provider).configured) { ctx.ui.notify(`Search authentication is missing. ${missingAuthGuidance(provider)}`, "warning"); } return true; } export function registerCodexSearchConfigCommand( pi: ExtensionAPI, onConfigured?: () => void, ): void { pi.registerCommand("codex-search-config", { description: "Configure remote Codex alpha 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 = getCodexSearchConfigPaths(ctx.cwd); ctx.ui.notify(`Global: ${paths.global}\nProject: ${paths.project}`, "info"); return; } if (command) { ctx.ui.notify( "Usage: /codex-search-config, /codex-search-config show, or /codex-search-config paths", "warning", ); return; } try { if (await configureInteractively(ctx)) onConfigured?.(); } catch (error) { const level = error instanceof CodexSearchConfigError ? "warning" : "error"; ctx.ui.notify(error instanceof Error ? error.message : String(error), level); } }, }); }