import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { VisionClawConfigSchema, GoogleTokensSchema, OwnerConfigSchema, TelegramAccessConfigSchema, ProviderSchema, inferModelFromProvider, DEFAULT_HEARTBEAT_INTERVAL_MS, LEGACY_HEARTBEAT_INTERVAL_MS, OBS_TUNNEL_DOMAIN, } from "./types.js";
import { CLAUDE_OPUS_4_6_MODEL, DEFAULT_CLAUDE_MODEL } from "./models.js";
const BASE_DIR = path.join(os.homedir(), ".visionclaw");
const PROFILES_DIR = path.join(BASE_DIR, "profiles");
let currentProfile = "default";
/**
 * Set the active profile. Must be called before any config access.
 * Profile names must be lowercase alphanumeric with hyphens.
 */
export function setProfile(name) {
    if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) {
        throw new Error(`Invalid profile name "${name}". Use lowercase letters, numbers, and hyphens.`);
    }
    currentProfile = name;
}
export function getProfile() {
    return currentProfile;
}
export function getBaseDir() {
    return BASE_DIR;
}
export function getProfilesDir() {
    return PROFILES_DIR;
}
export function getConfigDir() {
    return path.join(PROFILES_DIR, currentProfile);
}
export function getConfigFile() {
    return path.join(getConfigDir(), "config.json");
}
export function getTokensFile() {
    return path.join(getConfigDir(), "google-tokens.json");
}
export function getOwnerFile() {
    return path.join(getConfigDir(), "owner.json");
}
export function getTelegramAccessFile() {
    return path.join(getConfigDir(), "telegram-access.json");
}
export function getSessionFile() {
    return path.join(getConfigDir(), "session.json");
}
export function getOnboardingSessionFile() {
    return path.join(getConfigDir(), "onboarding-session.json");
}
export function getSkillsDir() {
    return path.join(getConfigDir(), ".claude", "skills");
}
export function getUserCatalogDir() {
    return path.join(getConfigDir(), "skill-catalog");
}
export function getMemoriesDir() {
    return path.join(getConfigDir(), "memories");
}
/**
 * Directory where the Claude Code CLI writes its **auto-memory** artifacts
 * (`CLAUDE.md`, per-scope memory files, etc.). Deliberately separate from
 * {@link getMemoriesDir} (which hosts our own SQLite-backed `memory.ts`)
 * so operators can tell at a glance which system owns which files.
 *
 * The path is relayed to the CLI via the `autoMemoryDirectory` field of
 * `<profile>/.claude/settings.local.json` — see
 * `seedClaudeAutoMemorySetting()` in `src/config/claude-settings.ts`. The
 * CLI expands `~/` on its side but here we keep an absolute path for
 * unambiguity.
 */
export function getClaudeAutoMemoryDir() {
    return path.join(getConfigDir(), "claude-auto-memory");
}
/**
 * Claude Code CLI **localSettings** file. Per the CLI Zod schema, settings
 * here override `userSettings` (`~/.claude/settings.json`) and, unlike
 * team-shared `projectSettings` (`.claude/settings.json`), are honored for
 * sensitive fields like `autoMemoryDirectory` (which the CLI deliberately
 * ignores in checked-in projectSettings for supply-chain reasons).
 *
 * Because the agent session runs with `cwd = getConfigDir()`, this path
 * is the one the CLI loads at session start.
 */
export function getClaudeLocalSettingsFile() {
    return path.join(getConfigDir(), ".claude", "settings.local.json");
}
export function getFailedUploadsDir() {
    return path.join(getConfigDir(), "failed-uploads");
}
export function getScreenshotsDir() {
    return path.join(os.homedir(), "screenshots");
}
export function getPlaywrightProfileDir() {
    return path.join(getConfigDir(), ".playwright-mcp");
}
export function getPlaywriterProfileDir() {
    return path.join(getConfigDir(), ".playwriter-chrome");
}
export function getProfilePhotosDir() {
    return path.join(getConfigDir(), "assets", "profile-photos");
}
/**
 * Directory used to materialize runtime-delivered tunnel credentials for the
 * active profile. This is the private home for `cloudflared` config.yml and
 * the `{UUID}.json` credentials blob when they are issued via the
 * onboarding-service heartbeat channel — deliberately distinct from the
 * user's shared `~/.cloudflared/` directory so multiple profiles and the
 * user's personal cloudflared setup don't collide.
 */
export function getProfileTunnelDir() {
    return path.join(getConfigDir(), "tunnel");
}
/**
 * Default filename for the profile's `cloudflared` config.yml.
 * Callers should prefer these helpers over constructing paths ad-hoc so that
 * any future directory migration stays centralized here.
 */
export function getProfileTunnelConfigPath() {
    return path.join(getProfileTunnelDir(), "config.yml");
}
export function getProfileTunnelCredentialsPath(tunnelId) {
    return path.join(getProfileTunnelDir(), `${tunnelId}.json`);
}
const PENDING_RESTART_FILE = "pending-restart.json";
export function writePendingRestart(info) {
    const filePath = path.join(getConfigDir(), PENDING_RESTART_FILE);
    fs.writeFileSync(filePath, JSON.stringify(info, null, 2), "utf-8");
}
export function consumePendingRestart() {
    const filePath = path.join(getConfigDir(), PENDING_RESTART_FILE);
    try {
        const raw = fs.readFileSync(filePath, "utf-8");
        fs.unlinkSync(filePath);
        return JSON.parse(raw);
    }
    catch {
        return null;
    }
}
const PENDING_UPGRADE_FILE = "pending-upgrade.json";
export function writePendingUpgrade(info) {
    const filePath = path.join(getConfigDir(), PENDING_UPGRADE_FILE);
    fs.writeFileSync(filePath, JSON.stringify(info, null, 2), "utf-8");
}
/**
 * Attach a remote command ID to the pending-upgrade marker so the agent
 * can auto-ack the command after restart.
 */
export function addCommandIdToPendingUpgrade(commandId) {
    const filePath = path.join(getConfigDir(), PENDING_UPGRADE_FILE);
    try {
        const raw = fs.readFileSync(filePath, "utf-8");
        const info = JSON.parse(raw);
        info.commandId = commandId;
        fs.writeFileSync(filePath, JSON.stringify(info, null, 2), "utf-8");
    }
    catch {
        // Marker may not exist (e.g. local checkout where no restart is scheduled).
    }
}
export function consumePendingUpgrade() {
    const filePath = path.join(getConfigDir(), PENDING_UPGRADE_FILE);
    try {
        const raw = fs.readFileSync(filePath, "utf-8");
        fs.unlinkSync(filePath);
        return JSON.parse(raw);
    }
    catch {
        return null;
    }
}
export function ensureConfigDir() {
    const configDir = getConfigDir();
    if (!fs.existsSync(configDir)) {
        fs.mkdirSync(configDir, { recursive: true });
    }
    const skillsDir = getSkillsDir();
    if (!fs.existsSync(skillsDir)) {
        fs.mkdirSync(skillsDir, { recursive: true });
    }
    const memoriesDir = getMemoriesDir();
    if (!fs.existsSync(memoriesDir)) {
        fs.mkdirSync(memoriesDir, { recursive: true });
    }
    const screenshotsDir = getScreenshotsDir();
    if (!fs.existsSync(screenshotsDir)) {
        fs.mkdirSync(screenshotsDir, { recursive: true });
    }
}
export function configExists() {
    return fs.existsSync(getConfigFile());
}
export function loadConfig() {
    const configFile = getConfigFile();
    if (!fs.existsSync(configFile)) {
        throw new Error(`Config not found at ${configFile}. Run 'visionclaw --profile ${currentProfile}' to start onboarding.`);
    }
    const rawText = fs.readFileSync(configFile, "utf-8");
    const raw = JSON.parse(rawText);
    const normalized = normalizeLegacyConfig(raw);
    const config = VisionClawConfigSchema.parse(normalized);
    const normalizedText = JSON.stringify(normalized, null, 2);
    if (normalizedText !== JSON.stringify(JSON.parse(rawText), null, 2)) {
        fs.writeFileSync(configFile, normalizedText, "utf-8");
    }
    return config;
}
export function saveConfig(config) {
    ensureConfigDir();
    fs.writeFileSync(getConfigFile(), JSON.stringify(config, null, 2), "utf-8");
}
function normalizeLegacyConfig(raw) {
    if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
        return raw;
    }
    const normalized = { ...raw };
    const parsedProvider = ProviderSchema.safeParse(normalized.provider);
    if (!("model" in normalized) && parsedProvider.success) {
        normalized.model = inferModelFromProvider(parsedProvider.data);
    }
    // Auto-upgrade: claude-opus-4-6 -> claude-opus-4-8
    if (normalized.model === CLAUDE_OPUS_4_6_MODEL) {
        normalized.model = DEFAULT_CLAUDE_MODEL;
    }
    // Auto-upgrade: 3-minute heartbeat -> 15-minute default
    if (normalized.heartbeatIntervalMs === LEGACY_HEARTBEAT_INTERVAL_MS) {
        normalized.heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS;
    }
    if ("wechatMonitorEnabled" in normalized && !("chatMonitorEnabled" in normalized)) {
        normalized.chatMonitorEnabled = normalized.wechatMonitorEnabled;
        delete normalized.wechatMonitorEnabled;
    }
    // Self-heal the `obs` block on every load.
    //
    // Trigger: `tunnelHostname` is missing OR outside OBS_TUNNEL_DOMAIN.
    //   Historical onboarding derived the zone from the agent's email TLD
    //   (e.g. `.dev`), producing hostnames like "<agent>.visionclawobs.dev" —
    //   a zone we never registered on Cloudflare. A missing value hits the
    //   same failure mode and shows up when `save-config` gave up with
    //   `tunnel: "off"` and cleared the field.
    //
    // Repair priority for the subdomain prefix (most → least trustworthy):
    //   (a) Salvage the existing subdomain from the bad hostname.
    //       It's already provisioned on Cloudflare's side, so keeping it
    //       avoids a rename on the named tunnel.
    //   (b) Fall back to `obs.tunnelName` — same reasoning: the named tunnel
    //       was already created under that name.
    //   (c) Last resort: derive from the local-part of `config.gmail`,
    //       applying the same sanitisation the onboarding service uses
    //       (`normalizeAgentName(...).replace(/-/g, "")`). This is the
    //       canonical mapping `hollingpugh@visionclaw.dev` → `hollingpugh`,
    //       so the resulting hostname matches what onboarding would have
    //       produced for this agent in the first place.
    //
    // Whenever we successfully produce a prefix (any of a/b/c), we also
    // force `enabled = true` and `tunnel = "cloudflared"`. Agents with a
    // broken hostname frequently got saved with `tunnel: "off"` after the
    // cloudflared DNS-route step failed against the broken zone; we unwind
    // that here.
    //
    // Configs whose `tunnelHostname` is already inside OBS_TUNNEL_DOMAIN are
    // left untouched — we never override explicit operator choices.
    if (typeof normalized.obs === "object" &&
        normalized.obs !== null &&
        !Array.isArray(normalized.obs)) {
        const obs = normalized.obs;
        const current = typeof obs.tunnelHostname === "string" ? obs.tunnelHostname : undefined;
        const isBad = current === undefined || !isInsideObsTunnelZone(current);
        if (isBad) {
            const subdomain = current ? extractUsableSubdomain(current) : undefined;
            const tunnelNameFallback = typeof obs.tunnelName === "string" && obs.tunnelName.trim() !== ""
                ? obs.tunnelName.trim()
                : undefined;
            const gmailFallback = typeof normalized.gmail === "string"
                ? extractPrefixFromGmail(normalized.gmail)
                : undefined;
            const agentPrefix = subdomain ?? tunnelNameFallback ?? gmailFallback;
            if (agentPrefix) {
                obs.tunnelHostname = `${agentPrefix}.${OBS_TUNNEL_DOMAIN}`;
                obs.enabled = true;
                obs.tunnel = "cloudflared";
            }
            else if (current !== undefined) {
                // Had a hostname but nothing recoverable and no gmail to synthesise
                // from — drop the bad value so we don't keep it around. Leave
                // enabled/tunnel alone because there's nothing we can do to make
                // the tunnel work here; the schema treats tunnelHostname as optional
                // and callers will recreate it via onboarding/reconfigure.
                delete obs.tunnelHostname;
            }
            // else: field was missing and no prefix source at all — nothing we
            // can safely do.
        }
        // Ensure `tunnelName` is present whenever `tunnelHostname` is.
        // The named-tunnel codepath (`startCloudflaredNamed`) requires both;
        // without `tunnelName` it silently falls back to an anonymous
        // trycloudflare.com URL even though the hostname is correct.
        // `tunnelName` is simply the first DNS label of `tunnelHostname`.
        const finalHostname = typeof obs.tunnelHostname === "string" ? obs.tunnelHostname : undefined;
        if (finalHostname &&
            (typeof obs.tunnelName !== "string" || obs.tunnelName.trim() === "")) {
            const dot = finalHostname.indexOf(".");
            if (dot > 0) {
                obs.tunnelName = finalHostname.slice(0, dot);
            }
        }
    }
    return normalized;
}
function isInsideObsTunnelZone(hostname) {
    return hostname.toLowerCase().endsWith(`.${OBS_TUNNEL_DOMAIN}`);
}
/**
 * Extract the first label of a tunnel hostname when it looks like a real
 * `<agent>.<zone>` pair. Returns `undefined` when:
 *   - the input has no dot at all ("visionclawobs"),
 *   - the first label equals the first label of our zone
 *     ("visionclawobs.dev" → first label is "visionclawobs", which would
 *      produce the nonsense "visionclawobs.visionclawobs.com"), or
 *   - the first label is empty (".foo").
 * In those degenerate cases the caller should fall back to `tunnelName`.
 */
function extractUsableSubdomain(hostname) {
    const dot = hostname.indexOf(".");
    if (dot <= 0)
        return undefined;
    const first = hostname.slice(0, dot);
    const zoneFirstLabel = OBS_TUNNEL_DOMAIN.split(".")[0];
    if (first.toLowerCase() === zoneFirstLabel.toLowerCase())
        return undefined;
    return first;
}
/**
 * Derive the tunnel subdomain prefix from the agent's gmail address.
 * We mirror the onboarding service's canonical transform
 * (`normalizeAgentName(agentName).replace(/-/g, "")`) so the resulting
 * hostname matches what onboarding would have provisioned for this
 * agent — onboarding's `gmail` is generated from the agent display
 * name via that same transform, so inverting it here lands on exactly
 * the subdomain Cloudflare already knows about.
 *
 * Extra hardening beyond what onboarding does: we also strip dots from
 * the local-part. Onboarding-generated gmails never contain dots (the
 * agent-name slug is hyphen-collapsed and hyphen-stripped), but a
 * manually-edited config or an operator-provided Google address like
 * `alice.smith@gmail.com` would otherwise yield `alice.smith`, which
 * produces an invalid multi-label hostname (`alice.smith.visionclawobs.com`)
 * that cloudflared can't route on our single-level zone. Stripping dots
 * keeps the output to a single DNS label in every case.
 *
 * Note: this runs before Zod schema parsing, so `gmail` is still
 * unvalidated `unknown`. Caller already gates on `typeof === "string"`.
 *
 * Returns `undefined` when:
 *   - the string is not a valid-looking email (no `@`),
 *   - the local-part is empty, or
 *   - sanitisation collapses the local-part to an empty string
 *     (e.g. "---@x.com" → "" after hyphen-strip).
 */
function extractPrefixFromGmail(gmail) {
    const at = gmail.indexOf("@");
    if (at <= 0)
        return undefined;
    const localPart = gmail.slice(0, at).trim().toLowerCase();
    // Mirror `normalizeAgentName` in services/onboarding/src/lib/db.ts
    // (collapse whitespace → hyphens, strip hyphens), plus strip dots to
    // guarantee a single DNS label. Order matters only for readability:
    // each replace is idempotent-ish, and none introduces characters the
    // next step would remove.
    const sanitised = localPart
        .replace(/\s+/g, "-")
        .replace(/-/g, "")
        .replace(/\./g, "");
    return sanitised === "" ? undefined : sanitised;
}
export function ownerConfigExists() {
    return fs.existsSync(getOwnerFile());
}
export function loadOwnerConfig() {
    const ownerFile = getOwnerFile();
    if (!fs.existsSync(ownerFile)) {
        throw new Error(`Owner config not found at ${ownerFile}. Run 'visionclaw set-owner --email <email>' first.`);
    }
    const rawText = fs.readFileSync(ownerFile, "utf-8");
    const raw = JSON.parse(rawText);
    const normalized = normalizeLegacyConfig(raw);
    const config = OwnerConfigSchema.parse(normalized);
    const normalizedText = JSON.stringify(normalized, null, 2);
    if (normalizedText !== JSON.stringify(JSON.parse(rawText), null, 2)) {
        fs.writeFileSync(ownerFile, normalizedText, "utf-8");
    }
    return config;
}
export function saveOwnerConfig(config) {
    ensureConfigDir();
    fs.writeFileSync(getOwnerFile(), JSON.stringify(config, null, 2), "utf-8");
}
export function loadTelegramAccessConfig() {
    const accessFile = getTelegramAccessFile();
    if (!fs.existsSync(accessFile)) {
        return TelegramAccessConfigSchema.parse({});
    }
    try {
        const raw = JSON.parse(fs.readFileSync(accessFile, "utf-8"));
        return TelegramAccessConfigSchema.parse(raw);
    }
    catch {
        return TelegramAccessConfigSchema.parse({});
    }
}
export function saveTelegramAccessConfig(config) {
    ensureConfigDir();
    fs.writeFileSync(getTelegramAccessFile(), JSON.stringify(config, null, 2), "utf-8");
}
export function isOnboardingComplete() {
    if (!ownerConfigExists())
        return false;
    try {
        const owner = loadOwnerConfig();
        return owner.onboardingComplete;
    }
    catch {
        return false;
    }
}
export function loadGoogleTokens() {
    const tokensFile = getTokensFile();
    if (!fs.existsSync(tokensFile)) {
        throw new Error(`Google tokens not found at ${tokensFile}. Run onboarding again.`);
    }
    const raw = JSON.parse(fs.readFileSync(tokensFile, "utf-8"));
    return GoogleTokensSchema.parse(raw);
}
export function saveGoogleTokens(tokens) {
    ensureConfigDir();
    fs.writeFileSync(getTokensFile(), JSON.stringify(tokens, null, 2), "utf-8");
}
export function loadSessionId(mode) {
    const sessionFile = getSessionFile();
    if (!fs.existsSync(sessionFile)) {
        return null;
    }
    try {
        const raw = JSON.parse(fs.readFileSync(sessionFile, "utf-8"));
        const parsed = raw;
        if (mode === "coding")
            return parsed.codingSessionId ?? null;
        if (mode === "general")
            return parsed.generalSessionId ?? parsed.sessionId ?? null;
        return parsed.generalSessionId ?? parsed.sessionId ?? null;
    }
    catch {
        return null;
    }
}
export function loadUsageSnapshot(mode) {
    const sessionFile = getSessionFile();
    if (!fs.existsSync(sessionFile))
        return null;
    try {
        const raw = JSON.parse(fs.readFileSync(sessionFile, "utf-8"));
        const parsed = raw;
        if (mode === "coding")
            return parsed.codingUsageSnapshot ?? null;
        return parsed.generalUsageSnapshot ?? parsed.usageSnapshot ?? null;
    }
    catch {
        return null;
    }
}
export function saveUsageSnapshot(snapshot, mode) {
    ensureConfigDir();
    const sessionFile = getSessionFile();
    let existing = {};
    if (fs.existsSync(sessionFile)) {
        try {
            existing = JSON.parse(fs.readFileSync(sessionFile, "utf-8"));
        }
        catch { /* start fresh */ }
    }
    if (mode === "coding") {
        existing.codingUsageSnapshot = snapshot;
    }
    else {
        existing.generalUsageSnapshot = snapshot;
        existing.usageSnapshot = snapshot;
    }
    existing.updatedAt = new Date().toISOString();
    fs.writeFileSync(sessionFile, JSON.stringify(existing, null, 2), "utf-8");
}
export function saveSessionId(sessionId, mode) {
    ensureConfigDir();
    const sessionFile = getSessionFile();
    let existing = {};
    if (fs.existsSync(sessionFile)) {
        try {
            existing = JSON.parse(fs.readFileSync(sessionFile, "utf-8"));
        }
        catch { /* start fresh */ }
    }
    const sessionMode = mode ?? "general";
    if (sessionMode === "coding") {
        existing.codingSessionId = sessionId;
    }
    else {
        existing.generalSessionId = sessionId;
        existing.sessionId = sessionId;
    }
    existing.previousSessionIds = prunePreviousSessionEntries(existing.previousSessionIds, sessionMode, sessionId);
    existing.updatedAt = new Date().toISOString();
    fs.writeFileSync(sessionFile, JSON.stringify(existing, null, 2), "utf-8");
}
const PREVIOUS_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
function isPreviousSessionEntry(value) {
    return typeof value === "object" && value !== null &&
        typeof value.id === "string" &&
        (value.mode === "general" || value.mode === "coding") &&
        typeof value.rotatedAt === "string";
}
function prunePreviousSessionEntries(entries, activeMode, activeSessionId) {
    if (!Array.isArray(entries))
        return [];
    const cutoff = Date.now() - PREVIOUS_SESSION_TTL_MS;
    return entries.filter((entry) => {
        if (!isPreviousSessionEntry(entry))
            return false;
        if (new Date(entry.rotatedAt).getTime() <= cutoff)
            return false;
        return !(entry.mode === activeMode && entry.id === activeSessionId);
    });
}
export function loadPreviousSessionIds() {
    const sessionFile = getSessionFile();
    if (!fs.existsSync(sessionFile))
        return [];
    try {
        const raw = JSON.parse(fs.readFileSync(sessionFile, "utf-8"));
        return prunePreviousSessionEntries(raw.previousSessionIds);
    }
    catch {
        return [];
    }
}
export function rotateSessionId(mode) {
    const currentId = loadSessionId(mode);
    if (!currentId)
        return;
    ensureConfigDir();
    const sessionFile = getSessionFile();
    let existing = {};
    if (fs.existsSync(sessionFile)) {
        try {
            existing = JSON.parse(fs.readFileSync(sessionFile, "utf-8"));
        }
        catch { /* start fresh */ }
    }
    const previous = prunePreviousSessionEntries(existing.previousSessionIds, mode, currentId);
    previous.push({ id: currentId, mode, rotatedAt: new Date().toISOString() });
    existing.previousSessionIds = previous;
    // Clear the active session ID for this mode
    if (mode === "coding") {
        delete existing.codingSessionId;
        delete existing.codingUsageSnapshot;
    }
    else {
        delete existing.generalSessionId;
        delete existing.sessionId;
        delete existing.generalUsageSnapshot;
        delete existing.usageSnapshot;
    }
    existing.updatedAt = new Date().toISOString();
    fs.writeFileSync(sessionFile, JSON.stringify(existing, null, 2), "utf-8");
}
export function loadOnboardingSessionId() {
    const file = getOnboardingSessionFile();
    if (!fs.existsSync(file))
        return null;
    try {
        const raw = JSON.parse(fs.readFileSync(file, "utf-8"));
        const parsed = raw;
        return parsed.sessionId ?? null;
    }
    catch {
        return null;
    }
}
export function saveOnboardingSessionId(sessionId) {
    ensureConfigDir();
    fs.writeFileSync(getOnboardingSessionFile(), JSON.stringify({ sessionId, updatedAt: new Date().toISOString() }, null, 2), "utf-8");
}
export function deleteOnboardingSession() {
    const file = getOnboardingSessionFile();
    if (fs.existsSync(file)) {
        fs.unlinkSync(file);
    }
}
export function getTOTPSecretsFile() {
    return path.join(getConfigDir(), "totp-secrets.json");
}
export function loadTOTPSecrets() {
    const file = getTOTPSecretsFile();
    if (!fs.existsSync(file))
        return [];
    try {
        const raw = JSON.parse(fs.readFileSync(file, "utf-8"));
        if (!Array.isArray(raw))
            return [];
        return raw;
    }
    catch {
        return [];
    }
}
export function saveTOTPSecrets(secrets) {
    ensureConfigDir();
    const file = getTOTPSecretsFile();
    fs.writeFileSync(file, JSON.stringify(secrets, null, 2), "utf-8");
    fs.chmodSync(file, 0o600);
}
// ---------------------------------------------------------------------------
// Dynamic MCP server persistence
// ---------------------------------------------------------------------------
export function getDynamicMcpServersFile() {
    return path.join(getConfigDir(), "mcp-servers.json");
}
/**
 * Load user-configured dynamic MCP servers from disk.
 * Returns an empty record if the file does not exist or is malformed.
 */
export function loadDynamicMcpServers() {
    const file = getDynamicMcpServersFile();
    if (!fs.existsSync(file))
        return {};
    try {
        const raw = JSON.parse(fs.readFileSync(file, "utf-8"));
        if (typeof raw !== "object" || raw === null || Array.isArray(raw))
            return {};
        return raw;
    }
    catch {
        return {};
    }
}
/**
 * Persist dynamic MCP server configs to disk.
 */
export function saveDynamicMcpServers(servers) {
    ensureConfigDir();
    fs.writeFileSync(getDynamicMcpServersFile(), JSON.stringify(servers, null, 2), "utf-8");
}
const DEFAULT_ONBOARDING_URL = "https://onboarding.visionclaw.dev";
/**
 * Build the pricing page URL from the agent's configured onboarding service URL.
 * Falls back to the production default when `onboardingServiceUrl` is not set.
 */
export function buildPricingUrl(config) {
    return `${(config.onboardingServiceUrl ?? DEFAULT_ONBOARDING_URL).replace(/\/+$/, "")}/pricing`;
}
//# sourceMappingURL=index.js.map