summaryrefslogtreecommitdiff
path: root/src/engine-target.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/engine-target.ts')
-rw-r--r--src/engine-target.ts79
1 files changed, 79 insertions, 0 deletions
diff --git a/src/engine-target.ts b/src/engine-target.ts
new file mode 100644
index 0000000..db4d81f
--- /dev/null
+++ b/src/engine-target.ts
@@ -0,0 +1,79 @@
+/**
+ * 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<EngineResolution, { kind: "invalid" }>): string =>
+ `unknown search engine "${failure.raw}" from ${sourceLabel(failure.source)} — ` +
+ `expected one of ${engineAliases().join(", ")}`;