import path from "node:path"; import { fileURLToPath } from "node:url"; import { config as loadDotenv } from "dotenv"; import { z } from "zod"; loadDotenv({ quiet: true }); loadDotenv({ path: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.env"), quiet: true }); const OptionalUrlSchema = z.preprocess( (value) => (typeof value === "string" && value.trim() === "" ? undefined : value), z.string().trim().url().optional() ); const ChatWebSearchEngineSchema = z.preprocess( (value) => { if (typeof value !== "string") return value; const trimmed = value.trim(); return trimmed ? trimmed.toLowerCase() : undefined; }, z.enum(["exa", "searxng"]).default("exa") ); const EnvSchema = z.object({ PORT: z.coerce.number().int().positive().default(8787), HOST: z.string().default("0.0.0.0"), // simple bearer-token auth for your personal backend ADMIN_TOKEN: z.string().min(20).optional(), // provider keys OPENAI_API_KEY: z.string().optional(), ANTHROPIC_API_KEY: z.string().optional(), XAI_API_KEY: z.string().optional(), EXA_API_KEY: z.string().optional(), // Chat-mode web_search tool configuration. Search mode remains Exa-only for now. CHAT_WEB_SEARCH_ENGINE: ChatWebSearchEngineSchema, SEARXNG_BASE_URL: OptionalUrlSchema, }).superRefine((value, ctx) => { if (value.CHAT_WEB_SEARCH_ENGINE === "searxng" && !value.SEARXNG_BASE_URL) { ctx.addIssue({ code: "custom", path: ["SEARXNG_BASE_URL"], message: "SEARXNG_BASE_URL is required when CHAT_WEB_SEARCH_ENGINE=searxng", }); } }); export type Env = z.infer; export const env: Env = EnvSchema.parse(process.env);