import fs from "node:fs"; import path from "node:path"; import { resolveStateDir } from "@/lib/clawdbot/paths"; import { defaultStudioSettings, mergeStudioSettings, normalizeStudioSettings, type StudioSettings, type StudioSettingsPatch, } from "@/lib/studio/settings"; // Studio settings are intentionally stored as a local JSON file for a single-user workflow. // That includes gateway connection details, so treat the state directory as plaintext secret // storage and document any changes to this threat model in README.md and SECURITY.md. const SETTINGS_DIRNAME = "claw3d"; const SETTINGS_FILENAME = "settings.json"; const OPENCLAW_CONFIG_FILENAME = "openclaw.json"; export const resolveStudioSettingsPath = () => path.join(resolveStateDir(), SETTINGS_DIRNAME, SETTINGS_FILENAME); const isRecord = (value: unknown): value is Record => Boolean(value && typeof value === "object"); const readOpenclawGatewayDefaults = (): { url: string; token: string } | null => { try { const configPath = path.join(resolveStateDir(), OPENCLAW_CONFIG_FILENAME); if (!fs.existsSync(configPath)) return null; const raw = fs.readFileSync(configPath, "utf8"); const parsed = JSON.parse(raw) as unknown; if (!isRecord(parsed)) return null; const gateway = isRecord(parsed.gateway) ? parsed.gateway : null; if (!gateway) return null; const auth = isRecord(gateway.auth) ? gateway.auth : null; const token = typeof auth?.token === "string" ? auth.token.trim() : ""; const port = typeof gateway.port === "number" && Number.isFinite(gateway.port) ? gateway.port : null; if (!token) return null; const url = port ? `ws://localhost:${port}` : ""; if (!url) return null; return { url, token }; } catch { return null; } }; export const loadLocalGatewayDefaults = (): { url: string; token: string } | null => { const fromFile = readOpenclawGatewayDefaults(); if (fromFile) return fromFile; // Fall back to env vars so operators can configure the gateway URL at // runtime without openclaw.json and without a rebuild. const envUrl = process.env.CLAW3D_GATEWAY_URL?.trim(); const envToken = process.env.CLAW3D_GATEWAY_TOKEN?.trim(); if (envUrl) return { url: envUrl, token: envToken ?? "" }; return null; }; export const loadStudioSettings = (): StudioSettings => { const settingsPath = resolveStudioSettingsPath(); if (!fs.existsSync(settingsPath)) { const defaults = defaultStudioSettings(); const gateway = loadLocalGatewayDefaults(); return gateway ? { ...defaults, gateway } : defaults; } const raw = fs.readFileSync(settingsPath, "utf8"); const parsed = JSON.parse(raw) as unknown; const settings = normalizeStudioSettings(parsed); if (!settings.gateway?.token) { const gateway = loadLocalGatewayDefaults(); if (gateway) { return { ...settings, gateway: settings.gateway?.url?.trim() ? { url: settings.gateway.url.trim(), token: gateway.token } : gateway, }; } } return settings; }; export const saveStudioSettings = (next: StudioSettings) => { const settingsPath = resolveStudioSettingsPath(); const dir = path.dirname(settingsPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(settingsPath, JSON.stringify(next, null, 2), "utf8"); }; export const applyStudioSettingsPatch = (patch: StudioSettingsPatch): StudioSettings => { const current = loadStudioSettings(); const next = mergeStudioSettings(current, patch); saveStudioSettings(next); return next; };