"use strict";
/**
 * Droid Config Manager
 *
 * Read/write ~/.factory/settings.json safely.
 * Only touches ccs-* prefixed entries in customModels[].
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.pruneOrphanedModels = exports.listCcsModels = exports.removeCcsModel = exports.upsertCcsModel = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const os = __importStar(require("os"));
const lockfile = __importStar(require("proper-lockfile"));
const model_catalog_1 = require("../cliproxy/model-catalog");
const thinking_validator_1 = require("../cliproxy/thinking-validator");
const extended_context_utils_1 = require("../shared/extended-context-utils");
const CCS_MODEL_PREFIX = 'ccs-';
const CCS_DISPLAY_PREFIX = 'CCS ';
/** Lock configuration for concurrent write safety */
const LOCK_STALE_MS = 10000;
const LOCK_RETRY_MIN_MS = 200;
const LOCK_RETRY_MAX_MS = 1000;
/**
 * Validate profile name to prevent filesystem/security issues.
 * Only alphanumeric, dot, underscore, hyphen allowed.
 */
function isValidProfileName(profile) {
    return !!profile && /^[a-zA-Z0-9._-]+$/.test(profile);
}
function validateProfileName(profile) {
    if (!isValidProfileName(profile)) {
        throw new Error(`Invalid profile name "${profile}": must contain only alphanumeric characters, dots, underscores, or hyphens`);
    }
}
const DROID_REASONING_OFF_VALUES = new Set(['off', 'none', 'disabled', '0']);
const DROID_ANTHROPIC_BUDGET_BY_EFFORT = {
    minimal: 4000,
    low: 4000,
    medium: 12000,
    high: 30000,
    max: 50000,
    xhigh: 64000,
    auto: 30000,
};
function isSupportedProvider(value) {
    return value === 'anthropic' || value === 'openai' || value === 'generic-chat-completion-api';
}
function isObject(value) {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isDroidCustomModelEntry(value) {
    if (!value || typeof value !== 'object')
        return false;
    const record = value;
    return (typeof record.displayName === 'string' &&
        record.displayName.trim() !== '' &&
        typeof record.model === 'string' &&
        typeof record.baseUrl === 'string' &&
        typeof record.apiKey === 'string' &&
        typeof record.provider === 'string' &&
        record.provider.trim() !== '');
}
function isManagedDisplayName(displayName) {
    return displayName.startsWith(CCS_DISPLAY_PREFIX) || displayName.startsWith(CCS_MODEL_PREFIX);
}
function parseManagedProfile(displayName) {
    let profile = null;
    if (displayName.startsWith(CCS_DISPLAY_PREFIX)) {
        profile = displayName.slice(CCS_DISPLAY_PREFIX.length).trim();
    }
    else if (displayName.startsWith(CCS_MODEL_PREFIX)) {
        profile = displayName.slice(CCS_MODEL_PREFIX.length).trim();
    }
    if (!profile || !isValidProfileName(profile))
        return null;
    return profile;
}
function asModelEntry(value) {
    return isDroidCustomModelEntry(value) ? value : null;
}
function isReasoningOffValue(value) {
    if (typeof value === 'number')
        return value <= 0;
    const normalized = value.trim().toLowerCase();
    return DROID_REASONING_OFF_VALUES.has(normalized);
}
function toAnthropicBudget(value) {
    if (typeof value === 'number') {
        return Math.max(1024, Math.floor(value));
    }
    const normalized = value.trim().toLowerCase();
    if (/^\d+$/.test(normalized)) {
        return Math.max(1024, Number.parseInt(normalized, 10));
    }
    return DROID_ANTHROPIC_BUDGET_BY_EFFORT[normalized] ?? DROID_ANTHROPIC_BUDGET_BY_EFFORT.high;
}
function resolveAnthropicModelId(model) {
    return (0, extended_context_utils_1.stripModelConfigurationSuffixes)(model);
}
function usesAnthropicAdaptiveThinking(model) {
    return (0, model_catalog_1.getModelThinkingSupport)('claude', resolveAnthropicModelId(model))?.type === 'levels';
}
function toAnthropicAdaptiveEffort(model, value) {
    const validation = (0, thinking_validator_1.validateThinking)('claude', resolveAnthropicModelId(model), value);
    if (isReasoningOffValue(validation.value)) {
        return undefined;
    }
    const normalized = String(validation.value).trim().toLowerCase();
    return normalized === 'auto' ? undefined : normalized;
}
function toReasoningEffort(value) {
    if (typeof value === 'number') {
        if (value <= 4000)
            return 'low';
        if (value <= 12000)
            return 'medium';
        if (value <= 30000)
            return 'high';
        if (value <= 50000)
            return 'max';
        return 'xhigh';
    }
    const normalized = value.trim().toLowerCase();
    if (!normalized)
        return 'high';
    return normalized;
}
function applyReasoningOverride(entry, provider, reasoningOverride) {
    const extraArgsKey = Object.prototype.hasOwnProperty.call(entry, 'extra_args')
        ? 'extra_args'
        : 'extraArgs';
    const currentExtraArgs = entry[extraArgsKey];
    const extraArgs = isObject(currentExtraArgs) ? { ...currentExtraArgs } : {};
    // Normalize legacy aliases before writing provider-specific shape.
    delete extraArgs.reasoningEffort;
    if (provider === 'anthropic') {
        delete extraArgs.reasoning;
        delete extraArgs.reasoning_effort;
        if (isReasoningOffValue(reasoningOverride)) {
            delete extraArgs.thinking;
            delete extraArgs.output_config;
        }
        else if (usesAnthropicAdaptiveThinking(entry.model)) {
            const thinking = isObject(extraArgs.thinking) ? { ...extraArgs.thinking } : {};
            const outputConfig = isObject(extraArgs.output_config) ? { ...extraArgs.output_config } : {};
            const effort = toAnthropicAdaptiveEffort(entry.model, reasoningOverride);
            thinking.type = 'adaptive';
            delete thinking.budget_tokens;
            delete thinking.budgetTokens;
            if (effort) {
                outputConfig.effort = effort;
            }
            else {
                delete outputConfig.effort;
            }
            extraArgs.thinking = thinking;
            if (Object.keys(outputConfig).length > 0) {
                extraArgs.output_config = outputConfig;
            }
            else {
                delete extraArgs.output_config;
            }
        }
        else {
            const thinking = isObject(extraArgs.thinking) ? { ...extraArgs.thinking } : {};
            thinking.type = 'enabled';
            thinking.budget_tokens = toAnthropicBudget(reasoningOverride);
            delete thinking.budgetTokens;
            extraArgs.thinking = thinking;
            delete extraArgs.output_config;
        }
    }
    else if (provider === 'openai') {
        delete extraArgs.reasoning_effort;
        delete extraArgs.thinking;
        if (isReasoningOffValue(reasoningOverride)) {
            delete extraArgs.reasoning;
        }
        else {
            const reasoning = isObject(extraArgs.reasoning) ? { ...extraArgs.reasoning } : {};
            reasoning.effort = toReasoningEffort(reasoningOverride);
            extraArgs.reasoning = reasoning;
        }
    }
    else {
        delete extraArgs.reasoning;
        delete extraArgs.thinking;
        if (isReasoningOffValue(reasoningOverride)) {
            delete extraArgs.reasoning_effort;
        }
        else {
            extraArgs.reasoning_effort = toReasoningEffort(reasoningOverride);
        }
    }
    if (Object.keys(extraArgs).length === 0) {
        delete entry.extraArgs;
        delete entry.extra_args;
        return;
    }
    entry[extraArgsKey] = extraArgs;
}
function buildSelectorAlias(displayName, index) {
    const normalizedDisplayName = displayName.trim().replace(/\s+/g, '-');
    return `${normalizedDisplayName}-${index}`;
}
function normalizeCustomModels(value) {
    if (Array.isArray(value)) {
        return value
            .map((entry) => asModelEntry(entry))
            .filter((entry) => !!entry);
    }
    // Accept legacy object-map shapes and normalize to array.
    if (value && typeof value === 'object') {
        return Object.values(value)
            .map((entry) => asModelEntry(entry))
            .filter((entry) => !!entry);
    }
    return [];
}
/**
 * Get path to ~/.factory/settings.json.
 * Respects CCS_HOME for test isolation (uses CCS_HOME/.factory/ in tests).
 */
function getFactoryDir() {
    const base = process.env.CCS_HOME || os.homedir();
    return path.join(base, '.factory');
}
function getSettingsPath() {
    return path.join(getFactoryDir(), 'settings.json');
}
/**
 * Ensure ~/.factory/ directory exists.
 */
function ensureFactoryDir() {
    const dir = getFactoryDir();
    try {
        fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
    }
    catch (err) {
        const error = err;
        if (error.code !== 'EEXIST')
            throw error;
    }
}
function getNoFollowFlag() {
    const candidate = fs.constants['O_NOFOLLOW'];
    if (process.platform !== 'win32' && typeof candidate === 'number') {
        return candidate;
    }
    return 0;
}
function openFileNoFollow(filePath, flags, mode) {
    const safeFlags = flags | getNoFollowFlag();
    if (mode === undefined) {
        return fs.openSync(filePath, safeFlags);
    }
    return fs.openSync(filePath, safeFlags, mode);
}
function readFileUtf8NoFollow(filePath) {
    const fd = openFileNoFollow(filePath, fs.constants.O_RDONLY);
    try {
        const stat = fs.fstatSync(fd);
        if (!stat.isFile()) {
            throw new Error('Refusing to read: settings.json is not a regular file');
        }
        return fs.readFileSync(fd, 'utf8');
    }
    finally {
        fs.closeSync(fd);
    }
}
/**
 * Read ~/.factory/settings.json, creating empty structure if missing.
 */
function readDroidSettings() {
    const settingsPath = getSettingsPath();
    if (!fs.existsSync(settingsPath)) {
        return { customModels: [] };
    }
    const fileStat = fs.lstatSync(settingsPath);
    if (fileStat.isSymbolicLink()) {
        throw new Error('Refusing to read: settings.json is a symlink');
    }
    if (!fileStat.isFile()) {
        throw new Error('Refusing to read: settings.json is not a regular file');
    }
    const raw = readFileUtf8NoFollow(settingsPath);
    try {
        const parsed = JSON.parse(raw);
        return {
            ...parsed,
            customModels: normalizeCustomModels(parsed.customModels),
        };
    }
    catch {
        // Corrupted file — preserve as backup, start fresh
        const backup = settingsPath + '.bak';
        try {
            fs.copyFileSync(settingsPath, backup);
            fs.chmodSync(backup, 0o600); // Secure backup permissions
            console.warn(`[!] Corrupted ${settingsPath}, backed up to ${backup}`);
        }
        catch (error) {
            console.warn(`[!] Corrupted ${settingsPath}; backup failed: ${error.message}`);
        }
        return { customModels: [] };
    }
}
async function acquireFactoryLock(retries) {
    ensureFactoryDir();
    const factoryDir = getFactoryDir();
    try {
        return await lockfile.lock(factoryDir, {
            stale: LOCK_STALE_MS,
            retries: { retries, minTimeout: LOCK_RETRY_MIN_MS, maxTimeout: LOCK_RETRY_MAX_MS },
        });
    }
    catch (error) {
        throw new Error(`Failed to lock Droid settings directory (${factoryDir}): ${error.message}`);
    }
}
function fsyncDir(dirPath) {
    try {
        const dirFd = fs.openSync(dirPath, fs.constants.O_RDONLY);
        try {
            fs.fsyncSync(dirFd);
        }
        finally {
            fs.closeSync(dirFd);
        }
    }
    catch {
        // Best-effort directory fsync (platform dependent).
    }
}
/**
 * Write ~/.factory/settings.json atomically with safe permissions.
 * Uses temp file + rename for atomicity on same filesystem.
 */
function writeDroidSettings(settings) {
    ensureFactoryDir();
    const settingsPath = getSettingsPath();
    // Refuse to write if target is a symlink (prevents symlink attacks)
    if (fs.existsSync(settingsPath)) {
        const stat = fs.lstatSync(settingsPath);
        if (stat.isSymbolicLink()) {
            throw new Error('Refusing to write: settings.json is a symlink');
        }
    }
    const tmpPath = settingsPath + '.tmp';
    if (fs.existsSync(tmpPath)) {
        const tmpStat = fs.lstatSync(tmpPath);
        if (tmpStat.isSymbolicLink()) {
            throw new Error('Refusing to write: settings.json.tmp is a symlink');
        }
    }
    const payload = JSON.stringify({
        ...settings,
        customModels: normalizeCustomModels(settings.customModels),
    }, null, 2);
    const fd = openFileNoFollow(tmpPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC, 0o600);
    try {
        const tmpFdStat = fs.fstatSync(fd);
        if (!tmpFdStat.isFile()) {
            throw new Error('Refusing to write: settings.json.tmp is not a regular file');
        }
        fs.writeFileSync(fd, payload + '\n', { encoding: 'utf8' });
        fs.fsyncSync(fd);
    }
    finally {
        fs.closeSync(fd);
    }
    fs.renameSync(tmpPath, settingsPath);
    fsyncDir(path.dirname(settingsPath));
    // Fix permissions on existing file if world-readable
    try {
        const stat = fs.statSync(settingsPath);
        if (stat.mode & 0o077) {
            fs.chmodSync(settingsPath, 0o600);
            console.warn('[!] Fixed permissions on ~/.factory/settings.json (was world-readable)');
        }
    }
    catch {
        // Best-effort permission check
    }
}
/**
 * Upsert a CCS-managed custom model entry.
 * Acquires file lock to prevent concurrent write races.
 */
async function upsertCcsModel(profile, model) {
    validateProfileName(profile);
    ensureFactoryDir();
    let release;
    let ref = null;
    try {
        release = await acquireFactoryLock(10);
        const settings = readDroidSettings();
        settings.customModels = normalizeCustomModels(settings.customModels);
        // Find existing current or legacy entry for this profile.
        const idx = settings.customModels.findIndex((m) => parseManagedProfile(m.displayName) === profile);
        const { reasoningOverride, ...modelWithoutReasoning } = model;
        const existingEntry = idx >= 0 ? settings.customModels[idx] : undefined;
        const entry = {
            ...(existingEntry ?? {}),
            ...modelWithoutReasoning,
            displayName: `CCS ${profile}`,
        };
        if (reasoningOverride !== undefined) {
            applyReasoningOverride(entry, model.provider, reasoningOverride);
        }
        if (idx >= 0) {
            settings.customModels[idx] = entry;
        }
        else {
            settings.customModels.push(entry);
        }
        const index = settings.customModels.findIndex((entry) => parseManagedProfile(entry.displayName) === profile);
        const safeIndex = index >= 0 ? index : 0;
        const selectorAlias = buildSelectorAlias(entry.displayName, safeIndex);
        const selector = `custom:${selectorAlias}`;
        // Droid interactive mode uses settings.model for default model selection.
        settings.model = selector;
        writeDroidSettings(settings);
        ref = {
            profile,
            displayName: entry.displayName,
            index: safeIndex,
            selectorAlias,
            selector,
        };
    }
    finally {
        if (release)
            await release();
    }
    return (ref || {
        profile,
        displayName: `CCS ${profile}`,
        index: 0,
        selectorAlias: `CCS-${profile}-0`,
        selector: `custom:CCS-${profile}-0`,
    });
}
exports.upsertCcsModel = upsertCcsModel;
/**
 * Remove a CCS-managed custom model entry.
 */
async function removeCcsModel(profile) {
    validateProfileName(profile);
    ensureFactoryDir();
    const settingsPath = getSettingsPath();
    let release;
    try {
        release = await acquireFactoryLock(3);
        if (!fs.existsSync(settingsPath))
            return;
        const settings = readDroidSettings();
        settings.customModels = normalizeCustomModels(settings.customModels);
        settings.customModels = settings.customModels.filter((m) => parseManagedProfile(m.displayName) !== profile);
        writeDroidSettings(settings);
    }
    finally {
        if (release)
            await release();
    }
}
exports.removeCcsModel = removeCcsModel;
/**
 * List all CCS-managed custom model entries.
 */
async function listCcsModels() {
    const result = new Map();
    const settings = readDroidSettings();
    for (const entry of normalizeCustomModels(settings.customModels)) {
        const profile = parseManagedProfile(entry.displayName);
        if (!profile)
            continue;
        if (!isSupportedProvider(entry.provider))
            continue;
        result.set(profile, {
            ...entry,
            provider: entry.provider,
        });
    }
    return result;
}
exports.listCcsModels = listCcsModels;
/**
 * Prune orphaned CCS entries from settings.json.
 * Removes ccs-* entries whose profile no longer exists in active profiles.
 */
async function pruneOrphanedModels(activeProfiles) {
    // Snapshot at call time so caller-side mutation cannot affect filtering while lock is pending.
    const activeProfilesSnapshot = [...activeProfiles];
    ensureFactoryDir();
    const settingsPath = getSettingsPath();
    let release;
    let removed = 0;
    try {
        release = await acquireFactoryLock(3);
        const activeProfileSet = new Set();
        for (const profile of activeProfilesSnapshot) {
            validateProfileName(profile);
            activeProfileSet.add(profile);
        }
        if (!fs.existsSync(settingsPath))
            return 0;
        const settings = readDroidSettings();
        settings.customModels = normalizeCustomModels(settings.customModels);
        const before = settings.customModels.length;
        settings.customModels = settings.customModels.filter((m) => {
            const profile = parseManagedProfile(m.displayName);
            if (profile) {
                return activeProfileSet.has(profile);
            }
            // Drop malformed managed entries; keep user-managed entries.
            return !isManagedDisplayName(m.displayName);
        });
        removed = before - settings.customModels.length;
        if (removed > 0) {
            writeDroidSettings(settings);
        }
    }
    finally {
        if (release)
            await release();
    }
    return removed;
}
exports.pruneOrphanedModels = pruneOrphanedModels;
//# sourceMappingURL=droid-config-manager.js.map