/** * Which search engine to use, resolved the same way the CDP host is: * environment variable, then a persisted choice, then a built-in default. * * Pure — no I/O — so the `/search-engine` command can report the configured * intent without touching disk twice, and so it is testable. */ import { type EngineId, parseEngineId, engineAliases } from "./engines.ts"; /** Used when nothing else says otherwise. */ export const DEFAULT_ENGINE: EngineId = "google"; /** The environment variable that pins the engine for one process. */ export const ENGINE_ENV_VAR = "CCS_SEARCH_ENGINE"; export type EngineSource = "parameter" | "env" | "stored" | "default"; export type EngineResolution = | { readonly kind: "ok"; readonly id: EngineId; readonly source: EngineSource } | { readonly kind: "invalid"; readonly raw: string; readonly source: EngineSource }; /** * Precedence: the per-call tool parameter, then `CCS_SEARCH_ENGINE`, then the * `/search-engine` choice, then the built-in default. * * Note the tool parameter sits *above* the environment variable, which is the * opposite of how the CDP target treats an explicit endpoint. That difference * is deliberate. For the browser, an environment variable has to win, because * quietly driving a different machine means automating the operator's own * signed-in Chrome — a safety property. Choosing a different search engine has * no equivalent hazard, and the ability to fall back to another engine when the * first one is serving a captcha is the single most useful thing the agent can * do with this tool. So the model is allowed to override the default; the * operator sets what it starts from. */ export const resolveEngine = ( parameter: string | null | undefined, env: NodeJS.ProcessEnv = process.env, stored: string | null = null, ): EngineResolution => { const candidates: Array<{ raw: string; source: EngineSource }> = []; const fromParam = parameter?.trim(); if (fromParam) candidates.push({ raw: fromParam, source: "parameter" }); const fromEnv = env[ENGINE_ENV_VAR]?.trim(); if (fromEnv) candidates.push({ raw: fromEnv, source: "env" }); const fromStore = stored?.trim(); if (fromStore) candidates.push({ raw: fromStore, source: "stored" }); for (const candidate of candidates) { const id = parseEngineId(candidate.raw); // An unparseable value is reported rather than skipped over. Falling // through to the next source would mean a typo in CCS_SEARCH_ENGINE // silently searches Google instead of saying so. if (!id) return { kind: "invalid", raw: candidate.raw, source: candidate.source }; return { kind: "ok", id, source: candidate.source }; } return { kind: "ok", id: DEFAULT_ENGINE, source: "default" }; }; const sourceLabel = (source: EngineSource): string => source === "parameter" ? "engine parameter" : source === "env" ? ENGINE_ENV_VAR : source === "stored" ? "/search-engine" : "built-in default"; export const describeEngineSource = (source: EngineSource): string => sourceLabel(source); /** One-line description of a bad engine name, listing what would have worked. */ export const describeEngineFailure = (failure: Extract): string => `unknown search engine "${failure.raw}" from ${sourceLabel(failure.source)} — ` + `expected one of ${engineAliases().join(", ")}`;