import path from "node:path"; import fs from "node:fs"; /** Minimal .env loader (no dependency). Does not override already-set variables. */ export function loadDotEnv(cwd = process.cwd()): void { const file = path.join(cwd, ".env"); if (!fs.existsSync(file)) return; for (const line of fs.readFileSync(file, "utf8").split("\n")) { const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/i); if (!m) continue; const key = m[1]!; let val = m[2]!; if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) val = val.slice(1, -1); if (process.env[key] === undefined) process.env[key] = val; } } export interface AppConfig { dataDir: string; profilesDir: string; sessionsDir: string; platformModelDir: string; mediaDir: string; databaseUrl?: string; headless: boolean; browserChannel: string; llmProvider: "none" | "anthropic" | "local"; llmModel: string; localLlmUrl?: string; localLlmKey?: string; localLlmModel?: string; dashboardPort: number; } export function loadConfig(): AppConfig { loadDotEnv(); const dataDir = path.resolve(process.env.SRC_DATA_DIR ?? "./data"); const cfg: AppConfig = { dataDir, profilesDir: path.join(dataDir, "browser_profiles"), sessionsDir: path.join(dataDir, "sessions"), platformModelDir: path.join(dataDir, "platform_model"), mediaDir: path.join(dataDir, "media"), databaseUrl: process.env.SRC_DATABASE_URL || undefined, headless: /^(1|true|yes)$/i.test(process.env.SRC_HEADLESS ?? "false"), browserChannel: process.env.SRC_BROWSER_CHANNEL ?? "chromium", llmProvider: (process.env.SRC_LLM_PROVIDER as AppConfig["llmProvider"]) ?? "none", llmModel: process.env.SRC_LLM_MODEL ?? "claude-opus-5", localLlmUrl: process.env.SRC_LOCAL_LLM_URL, localLlmKey: process.env.SRC_LOCAL_LLM_KEY, localLlmModel: process.env.SRC_LOCAL_LLM_MODEL, dashboardPort: Number(process.env.SRC_DASHBOARD_PORT ?? 8340), }; for (const d of [cfg.profilesDir, cfg.sessionsDir, cfg.platformModelDir, cfg.mediaDir]) fs.mkdirSync(d, { recursive: true }); return cfg; }