// Live-reloading config layer. Mirrors Recap's config.js pattern: read // /data/config/relay-config.json on every access (filesystem watcher // pulls in StartOS-action changes without a daemon restart), parse, // and expose typed accessors. // // All defaults match the schema in startos/file-models/config.json.ts. import fs from "fs/promises"; import path from "path"; let dataDir = "/data"; let cached = { mtimeMs: 0, snapshot: defaultConfig() }; function defaultConfig() { return { relay_gemini_api_key: "", relay_parakeet_base_url: "", relay_gemma_base_url: "", relay_parakeet_model: "parakeet-tdt-0.6b-v3", relay_gemma_model: "gemma3:27b", relay_keysat_base_url: "https://keysat.xyz", relay_admin_username: "", relay_admin_password_hash: "", relay_admin_password_salt: "", relay_admin_session_secret: "", relay_tier_quotas_json: JSON.stringify({ core: { lifetime: 10, geminiCapLifetime: 5, monthly: null, geminiCapMonthly: null, }, pro: { lifetime: null, monthly: 50, geminiCapMonthly: 25 }, max: { lifetime: null, monthly: null, geminiCapMonthly: 50 }, }), }; } function configPath() { return path.join(dataDir, "config", "relay-config.json"); } export async function initConfig({ dataDir: dd }) { if (dd) dataDir = dd; await fs.mkdir(path.dirname(configPath()), { recursive: true }).catch(() => {}); // Prime the cache so the first request doesn't pay for a file-read. await getConfigSnapshot(); } // Reads the on-disk config and merges with defaults. Cheap — single // stat + read per call, but the result is cached until the file mtime // changes so repeat callers within one request don't re-read. export async function getConfigSnapshot() { const p = configPath(); let stat; try { stat = await fs.stat(p); } catch { return cached.snapshot; } if (stat.mtimeMs === cached.mtimeMs) return cached.snapshot; try { const raw = await fs.readFile(p, "utf8"); const parsed = JSON.parse(raw); cached = { mtimeMs: stat.mtimeMs, snapshot: { ...defaultConfig(), ...parsed }, }; } catch (err) { console.warn(`[config] failed to parse ${p}: ${err?.message}`); } return cached.snapshot; } // Parsed view of relay_tier_quotas_json, with safe fallbacks if the // blob is missing or malformed. geminiCapLifetime is the new field // added in relay 0.2.3 — splits a Core install's lifetime budget into // Gemini-served vs hardware-served credits. export async function getTierQuotas() { const cfg = await getConfigSnapshot(); try { const parsed = JSON.parse(cfg.relay_tier_quotas_json); return { core: { lifetime: parsed?.core?.lifetime ?? 10, geminiCapLifetime: parsed?.core?.geminiCapLifetime ?? 5, monthly: parsed?.core?.monthly ?? null, geminiCapMonthly: parsed?.core?.geminiCapMonthly ?? null, }, pro: { lifetime: parsed?.pro?.lifetime ?? null, monthly: parsed?.pro?.monthly ?? 50, geminiCapMonthly: parsed?.pro?.geminiCapMonthly ?? 25, }, max: { lifetime: parsed?.max?.lifetime ?? null, monthly: parsed?.max?.monthly ?? null, geminiCapMonthly: parsed?.max?.geminiCapMonthly ?? 50, }, }; } catch { return { core: { lifetime: 10, geminiCapLifetime: 5, monthly: null, geminiCapMonthly: null, }, pro: { lifetime: null, monthly: 50, geminiCapMonthly: 25 }, max: { lifetime: null, monthly: null, geminiCapMonthly: 50 }, }; } }