import { z } from 'zod'; const ConfigSchema = z.object({ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), PORT: z.coerce.number().int().positive().default(3000), HOST: z.string().default('0.0.0.0'), DATABASE_URL: z.string().url(), LLAMA_SWAP_URL: z.string().url(), PROJECT_ROOT_WHITELIST: z.string().default('/opt'), BOOTSTRAP_ROOT: z.string().default('/opt/projects'), DEFAULT_MODEL: z.string().default('qwen3.6-35b-a3b-mxfp4'), LOG_LEVEL: z.string().default('info'), // v1.11.8: SearXNG JSON endpoint for web_search / web_fetch tools. // Defaults to the internal Tailscale Fathom URL (bypasses Authelia). // The public search.indifferentketchup.com URL would 302 to auth and // is unusable from the server context — keep the internal one. SEARXNG_URL: z.string().url().default('http://100.114.205.53:8888'), GITEA_BASE_URL: z.string().url().default('https://git.indifferentketchup.com'), GITEA_USER: z.string().default('indifferentketchup'), GITEA_TOKEN: z.string().optional(), GITEA_SSH_HOST: z.string().default('100.114.205.53:2222'), // v1.15.0-mcp-multi: path to the MCP config JSON file. Default /data/mcp.json // (bind-mounted alongside AGENTS.md). File missing = no MCP (opt-in). MCP_CONFIG_PATH: z.string().optional(), // v2.0.5: cheaper model for titles, summaries, labeling. Falls back to // session model (auto_name) or DEFAULT_MODEL when unset. FAST_MODEL: z.string().optional(), }); export type Config = z.infer; let cached: Config | null = null; export function loadConfig(): Config { if (cached) return cached; const parsed = ConfigSchema.safeParse(process.env); if (!parsed.success) { console.error('Invalid environment configuration:'); console.error(parsed.error.flatten().fieldErrors); process.exit(1); } cached = parsed.data; return cached; }