dist-mcp / services / customConfigsLoader.js
dist-mcp / services / customConfigsLoader.js
"use strict";
/**
* Custom Configs Loader
* Loads and manages user-defined presets from Draw Things app's custom_configs.json
*
* Priority System:
* 1. Custom Configs (this file) - PRIORITY 1
* 2. Model Overlays (modelOverlays.ts) - PRIORITY 2 (fallback)
* 3. Defaults (defaultParamsDrawThings*.ts) - PRIORITY 3 (base)
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkCustomConfigs = checkCustomConfigs;
exports.loadCustomConfigs = loadCustomConfigs;
exports.getCustomPreset = getCustomPreset;
exports.clearCustomConfigsCache = clearCustomConfigsCache;
exports.getEffectiveOverlay = getEffectiveOverlay;
exports.getAvailableCustomCombinations = getAvailableCustomCombinations;
exports.getCustomPresetLabelsUsingModelFilename = getCustomPresetLabelsUsingModelFilename;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const os_1 = __importDefault(require("os"));
const modelOverlays_js_1 = require("./modelOverlays.js");
// Singleton cache for loaded presets
let cachedPresets = null;
let configFilePath = null;
let didWarnCacheUninitialized = false;
let didLogUsingPreset = new Set();
function isVerboseCustomConfigsLoggingEnabled() {
// Keep per-call overlay logging OFF by default. This function intentionally
// does not depend on draw-things-chat config wiring so that optional
// consumers (e.g. draw-things-index) remain quiet.
try {
return process?.env?.DTC_CUSTOM_CONFIGS_VERBOSE === "1";
}
catch {
return false;
}
}
function getPresetsCacheOrNull() {
// If the caller never configured custom configs (no path), treat as disabled.
// This is important for optional consumers (e.g. draw-things-index) that
// import helpers without running draw-things-chat's startup sequence.
if (cachedPresets === null && !configFilePath) {
cachedPresets = new Map();
}
return cachedPresets;
}
/**
* Check if custom_configs.json exists and is readable
* Called ONCE during main() startup
*/
async function checkCustomConfigs(customPath) {
try {
// This function is intentionally controlled by LM Studio config (src/config.ts).
// We do NOT auto-fallback to a default path here.
cachedPresets = null;
const rawPath = typeof customPath === "string" ? customPath.trim() : "";
if (!rawPath) {
configFilePath = null;
cachedPresets = new Map();
didWarnCacheUninitialized = false;
return {
available: false,
filePath: null,
};
}
// Expand tilde
const expandedPath = rawPath.startsWith("~")
? path_1.default.join(os_1.default.homedir(), rawPath.slice(1))
: rawPath;
configFilePath = expandedPath;
didWarnCacheUninitialized = false;
// Check existence and readability
await fs_1.default.promises.access(expandedPath, fs_1.default.constants.R_OK);
return {
available: true,
filePath: expandedPath,
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
return {
available: false,
filePath: null,
error: errorMessage,
};
}
}
/**
* Load all custom presets from custom_configs.json
* Returns Map<presetName, CustomPreset>
* Caches result for performance
*/
async function loadCustomConfigs() {
// Return cached if available
if (cachedPresets !== null) {
return cachedPresets;
}
const presets = new Map();
try {
if (!configFilePath) {
// Not configured / disabled.
cachedPresets = presets;
return presets;
}
// Read and parse JSON
const fileContent = await fs_1.default.promises.readFile(configFilePath, "utf-8");
const rawPresets = JSON.parse(fileContent);
if (!Array.isArray(rawPresets)) {
console.warn("[CustomConfigs] Expected array of presets, got:", typeof rawPresets);
cachedPresets = presets;
return presets;
}
// Parse each preset
for (const rawPreset of rawPresets) {
try {
const preset = parsePreset(rawPreset);
if (preset) {
presets.set(preset.name, preset);
}
}
catch (error) {
// Silently skip invalid presets (Draw Things allows freeform names)
}
}
if (presets.size > 0) {
const presetNames = Array.from(presets.keys()).join(", ");
console.log(`[CustomConfigs] Loaded ${presets.size} presets: ${presetNames}`);
}
else {
console.log(`[CustomConfigs] No valid presets found in ${configFilePath}`);
}
}
catch (error) {
console.warn("[CustomConfigs] Failed to load custom_configs.json:", error instanceof Error ? error.message : error);
}
// Cache result (even if empty)
cachedPresets = presets;
didWarnCacheUninitialized = false;
return presets;
}
/**
* Convert Draw Things camelCase parameter names to our snake_case
* Handles nested objects (e.g., loras array)
*/
function convertParamNames(config) {
const converted = {};
for (const [key, value] of Object.entries(config)) {
// Convert camelCase to snake_case
// Handles cases like: guidanceScale → guidance_scale, clipLText → clip_l_text
const snakeKey = key
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
.replace(/([A-Z])([A-Z][a-z])/g, "$1_$2")
.toLowerCase();
// Handle nested arrays (e.g., loras)
if (Array.isArray(value)) {
converted[snakeKey] = value.map((item) => typeof item === "object" && item !== null
? convertParamNames(item)
: item);
}
else if (typeof value === "object" && value !== null) {
// Handle nested objects
converted[snakeKey] = convertParamNames(value);
}
else {
converted[snakeKey] = value;
}
}
return converted;
}
/**
* Parse a single preset from Draw Things format
* Converts camelCase configuration to snake_case
*/
function parsePreset(rawPreset) {
const { name, configuration } = rawPreset;
if (!name || typeof name !== "string") {
console.warn("[CustomConfigs] Preset missing name field");
return null;
}
// Parse name format: "mode.modelId" (e.g., "text2image.flux")
const parts = name.split(".");
if (parts.length !== 2) {
// Silently skip - Draw Things allows freeform preset names
return null;
}
const [modeRaw, modelId] = parts;
// Map Draw Things mode names to our internal mode types
const modeMap = {
text2image: "txt2img",
image2image: "img2img",
edit: "edit",
text2video: "txt2vid",
image2video: "img2vid",
};
const mode = modeMap[modeRaw];
if (!mode) {
// Silently skip - only process text2image/image2image/edit presets
return null;
}
// Convert camelCase configuration to snake_case
const convertedConfig = convertParamNames(configuration);
// Cast to our params type (will be validated by filterCustomPresetParams in Task 1.4)
const params = convertedConfig;
return {
name,
mode,
modelId,
params,
};
}
/**
* Get specific preset by notation (e.g., "text2image.flux")
* Returns null if not found
*/
function getCustomPreset(notation) {
const cache = getPresetsCacheOrNull();
if (!cache) {
if (!didWarnCacheUninitialized) {
console.warn("[CustomConfigs] Cache not initialized, call loadCustomConfigs() first");
didWarnCacheUninitialized = true;
}
return null;
}
return cache.get(notation) ?? null;
}
/**
* Clear cache (for testing or hot-reload scenarios)
*/
function clearCustomConfigsCache() {
cachedPresets = null;
configFilePath = null;
didWarnCacheUninitialized = false;
didLogUsingPreset = new Set();
}
/**
* Get effective overlay for mode + model combination
*
* Priority:
* 1. Custom Config (if available) - ALWAYS checked first
* 2. Model Overlay (existing system)
* 3. null (use defaults)
*
* IMPORTANT: Custom Configs are checked even when modelId is undefined.
* In that case, we default to "auto" as the effective model identifier.
* This ensures that Custom Configs like "text2image.auto" are always used
* when available, even if the user doesn't explicitly pass model="auto".
*/
function getEffectiveOverlay(modelId, mode) {
// Step 1: Check custom configs (PRIORITY 1)
// ALWAYS check Custom Configs, even if modelId is undefined!
// Default to "auto" as the effective model identifier.
const cache = getPresetsCacheOrNull();
if (cache) {
const effectiveModelId = modelId ?? "auto";
const notation = buildPresetNotation(mode, effectiveModelId);
const verbose = isVerboseCustomConfigsLoggingEnabled();
if (verbose) {
console.debug(`[CustomConfigs] getEffectiveOverlay: modelId=${effectiveModelId}, mode=${mode}, notation=${notation}, cacheSize=${cache.size}`);
}
const customPreset = getCustomPreset(notation);
if (customPreset) {
// Avoid spamming logs when called repeatedly (e.g. metadata resolver loops).
if (verbose && !didLogUsingPreset.has(notation)) {
console.info(`[CustomConfigs] Using custom preset '${notation}'`);
didLogUsingPreset.add(notation);
}
return {
source: "custom",
presetName: notation,
params: customPreset.params,
};
}
else {
if (verbose) {
console.debug(`[CustomConfigs] Custom preset '${notation}' not found, falling back to model overlay`);
}
}
}
else {
// Only warn if a config path exists (i.e. the feature is enabled), and de-dupe spam.
if (configFilePath && !didWarnCacheUninitialized) {
console.warn(`[CustomConfigs] cachedPresets is null (loadCustomConfigs() was not called or failed)`);
didWarnCacheUninitialized = true;
}
}
// Step 2: Fall back to model overlays (PRIORITY 2)
const modelOverlay = (0, modelOverlays_js_1.getModelOverlay)(modelId, mode);
if (modelOverlay) {
return {
source: "modelOverlay",
params: modelOverlay,
};
}
// Step 3: Use defaults
return {
source: "default",
params: null,
};
}
/**
* Build preset notation from mode and modelId
* Examples:
* txt2img + flux → "text2image.flux"
* img2img + custom → "image2image.custom"
* edit + z-image → "edit.z-image"
*/
function buildPresetNotation(mode, modelId) {
const modeMap = {
txt2img: "text2image",
img2img: "image2image",
edit: "edit",
txt2vid: "text2video",
img2vid: "image2video",
};
return `${modeMap[mode]}.${modelId}`;
}
/**
* Get all available mode+model combinations from Custom Configs
* Returns array of tuples [mode, modelId]
* Used for error handling to show what's available
*/
function getAvailableCustomCombinations() {
const cache = getPresetsCacheOrNull();
if (!cache)
return [];
const combinations = [];
for (const preset of cache.values()) {
// Map internal mode back to user-facing mode
const userMode = preset.mode === "txt2img"
? "text2image"
: preset.mode === "img2img"
? "image2image"
: preset.mode === "txt2vid"
? "text2video"
: preset.mode === "img2vid"
? "image2video"
: "edit";
combinations.push([userMode, preset.modelId]);
}
return combinations;
}
/**
* Return the Draw Things custom preset names that explicitly reference a given model filename.
*
* NOTE: Many presets do not set `model` explicitly; those cannot be matched here.
* This is intended for optional, display-only enrichment.
*/
function getCustomPresetLabelsUsingModelFilename(modelFilenameOrPath) {
const cache = getPresetsCacheOrNull();
if (!cache)
return [];
if (typeof modelFilenameOrPath !== "string")
return [];
const wanted = path_1.default.basename(modelFilenameOrPath).trim().toLowerCase();
if (!wanted)
return [];
const labels = new Set();
for (const preset of cache.values()) {
const presetModel = preset.params?.model;
if (typeof presetModel !== "string")
continue;
const presetBase = path_1.default.basename(presetModel).trim().toLowerCase();
if (presetBase === wanted) {
labels.add(preset.name);
}
}
return Array.from(labels).sort();
}
"use strict";
/**
* Custom Configs Loader
* Loads and manages user-defined presets from Draw Things app's custom_configs.json
*
* Priority System:
* 1. Custom Configs (this file) - PRIORITY 1
* 2. Model Overlays (modelOverlays.ts) - PRIORITY 2 (fallback)
* 3. Defaults (defaultParamsDrawThings*.ts) - PRIORITY 3 (base)
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkCustomConfigs = checkCustomConfigs;
exports.loadCustomConfigs = loadCustomConfigs;
exports.getCustomPreset = getCustomPreset;
exports.clearCustomConfigsCache = clearCustomConfigsCache;
exports.getEffectiveOverlay = getEffectiveOverlay;
exports.getAvailableCustomCombinations = getAvailableCustomCombinations;
exports.getCustomPresetLabelsUsingModelFilename = getCustomPresetLabelsUsingModelFilename;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const os_1 = __importDefault(require("os"));
const modelOverlays_js_1 = require("./modelOverlays.js");
// Singleton cache for loaded presets
let cachedPresets = null;
let configFilePath = null;
let didWarnCacheUninitialized = false;
let didLogUsingPreset = new Set();
function isVerboseCustomConfigsLoggingEnabled() {
// Keep per-call overlay logging OFF by default. This function intentionally
// does not depend on draw-things-chat config wiring so that optional
// consumers (e.g. draw-things-index) remain quiet.
try {
return process?.env?.DTC_CUSTOM_CONFIGS_VERBOSE === "1";
}
catch {
return false;
}
}
function getPresetsCacheOrNull() {
// If the caller never configured custom configs (no path), treat as disabled.
// This is important for optional consumers (e.g. draw-things-index) that
// import helpers without running draw-things-chat's startup sequence.
if (cachedPresets === null && !configFilePath) {
cachedPresets = new Map();
}
return cachedPresets;
}
/**
* Check if custom_configs.json exists and is readable
* Called ONCE during main() startup
*/
async function checkCustomConfigs(customPath) {
try {
// This function is intentionally controlled by LM Studio config (src/config.ts).
// We do NOT auto-fallback to a default path here.
cachedPresets = null;
const rawPath = typeof customPath === "string" ? customPath.trim() : "";
if (!rawPath) {
configFilePath = null;
cachedPresets = new Map();
didWarnCacheUninitialized = false;
return {
available: false,
filePath: null,
};
}
// Expand tilde
const expandedPath = rawPath.startsWith("~")
? path_1.default.join(os_1.default.homedir(), rawPath.slice(1))
: rawPath;
configFilePath = expandedPath;
didWarnCacheUninitialized = false;
// Check existence and readability
await fs_1.default.promises.access(expandedPath, fs_1.default.constants.R_OK);
return {
available: true,
filePath: expandedPath,
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
return {
available: false,
filePath: null,
error: errorMessage,
};
}
}
/**
* Load all custom presets from custom_configs.json
* Returns Map<presetName, CustomPreset>
* Caches result for performance
*/
async function loadCustomConfigs() {
// Return cached if available
if (cachedPresets !== null) {
return cachedPresets;
}
const presets = new Map();
try {
if (!configFilePath) {
// Not configured / disabled.
cachedPresets = presets;
return presets;
}
// Read and parse JSON
const fileContent = await fs_1.default.promises.readFile(configFilePath, "utf-8");
const rawPresets = JSON.parse(fileContent);
if (!Array.isArray(rawPresets)) {
console.warn("[CustomConfigs] Expected array of presets, got:", typeof rawPresets);
cachedPresets = presets;
return presets;
}
// Parse each preset
for (const rawPreset of rawPresets) {
try {
const preset = parsePreset(rawPreset);
if (preset) {
presets.set(preset.name, preset);
}
}
catch (error) {
// Silently skip invalid presets (Draw Things allows freeform names)
}
}
if (presets.size > 0) {
const presetNames = Array.from(presets.keys()).join(", ");
console.log(`[CustomConfigs] Loaded ${presets.size} presets: ${presetNames}`);
}
else {
console.log(`[CustomConfigs] No valid presets found in ${configFilePath}`);
}
}
catch (error) {
console.warn("[CustomConfigs] Failed to load custom_configs.json:", error instanceof Error ? error.message : error);
}
// Cache result (even if empty)
cachedPresets = presets;
didWarnCacheUninitialized = false;
return presets;
}
/**
* Convert Draw Things camelCase parameter names to our snake_case
* Handles nested objects (e.g., loras array)
*/
function convertParamNames(config) {
const converted = {};
for (const [key, value] of Object.entries(config)) {
// Convert camelCase to snake_case
// Handles cases like: guidanceScale → guidance_scale, clipLText → clip_l_text
const snakeKey = key
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
.replace(/([A-Z])([A-Z][a-z])/g, "$1_$2")
.toLowerCase();
// Handle nested arrays (e.g., loras)
if (Array.isArray(value)) {
converted[snakeKey] = value.map((item) => typeof item === "object" && item !== null
? convertParamNames(item)
: item);
}
else if (typeof value === "object" && value !== null) {
// Handle nested objects
converted[snakeKey] = convertParamNames(value);
}
else {
converted[snakeKey] = value;
}
}
return converted;
}
/**
* Parse a single preset from Draw Things format
* Converts camelCase configuration to snake_case
*/
function parsePreset(rawPreset) {
const { name, configuration } = rawPreset;
if (!name || typeof name !== "string") {
console.warn("[CustomConfigs] Preset missing name field");
return null;
}
// Parse name format: "mode.modelId" (e.g., "text2image.flux")
const parts = name.split(".");
if (parts.length !== 2) {
// Silently skip - Draw Things allows freeform preset names
return null;
}
const [modeRaw, modelId] = parts;
// Map Draw Things mode names to our internal mode types
const modeMap = {
text2image: "txt2img",
image2image: "img2img",
edit: "edit",
text2video: "txt2vid",
image2video: "img2vid",
};
const mode = modeMap[modeRaw];
if (!mode) {
// Silently skip - only process text2image/image2image/edit presets
return null;
}
// Convert camelCase configuration to snake_case
const convertedConfig = convertParamNames(configuration);
// Cast to our params type (will be validated by filterCustomPresetParams in Task 1.4)
const params = convertedConfig;
return {
name,
mode,
modelId,
params,
};
}
/**
* Get specific preset by notation (e.g., "text2image.flux")
* Returns null if not found
*/
function getCustomPreset(notation) {
const cache = getPresetsCacheOrNull();
if (!cache) {
if (!didWarnCacheUninitialized) {
console.warn("[CustomConfigs] Cache not initialized, call loadCustomConfigs() first");
didWarnCacheUninitialized = true;
}
return null;
}
return cache.get(notation) ?? null;
}
/**
* Clear cache (for testing or hot-reload scenarios)
*/
function clearCustomConfigsCache() {
cachedPresets = null;
configFilePath = null;
didWarnCacheUninitialized = false;
didLogUsingPreset = new Set();
}
/**
* Get effective overlay for mode + model combination
*
* Priority:
* 1. Custom Config (if available) - ALWAYS checked first
* 2. Model Overlay (existing system)
* 3. null (use defaults)
*
* IMPORTANT: Custom Configs are checked even when modelId is undefined.
* In that case, we default to "auto" as the effective model identifier.
* This ensures that Custom Configs like "text2image.auto" are always used
* when available, even if the user doesn't explicitly pass model="auto".
*/
function getEffectiveOverlay(modelId, mode) {
// Step 1: Check custom configs (PRIORITY 1)
// ALWAYS check Custom Configs, even if modelId is undefined!
// Default to "auto" as the effective model identifier.
const cache = getPresetsCacheOrNull();
if (cache) {
const effectiveModelId = modelId ?? "auto";
const notation = buildPresetNotation(mode, effectiveModelId);
const verbose = isVerboseCustomConfigsLoggingEnabled();
if (verbose) {
console.debug(`[CustomConfigs] getEffectiveOverlay: modelId=${effectiveModelId}, mode=${mode}, notation=${notation}, cacheSize=${cache.size}`);
}
const customPreset = getCustomPreset(notation);
if (customPreset) {
// Avoid spamming logs when called repeatedly (e.g. metadata resolver loops).
if (verbose && !didLogUsingPreset.has(notation)) {
console.info(`[CustomConfigs] Using custom preset '${notation}'`);
didLogUsingPreset.add(notation);
}
return {
source: "custom",
presetName: notation,
params: customPreset.params,
};
}
else {
if (verbose) {
console.debug(`[CustomConfigs] Custom preset '${notation}' not found, falling back to model overlay`);
}
}
}
else {
// Only warn if a config path exists (i.e. the feature is enabled), and de-dupe spam.
if (configFilePath && !didWarnCacheUninitialized) {
console.warn(`[CustomConfigs] cachedPresets is null (loadCustomConfigs() was not called or failed)`);
didWarnCacheUninitialized = true;
}
}
// Step 2: Fall back to model overlays (PRIORITY 2)
const modelOverlay = (0, modelOverlays_js_1.getModelOverlay)(modelId, mode);
if (modelOverlay) {
return {
source: "modelOverlay",
params: modelOverlay,
};
}
// Step 3: Use defaults
return {
source: "default",
params: null,
};
}
/**
* Build preset notation from mode and modelId
* Examples:
* txt2img + flux → "text2image.flux"
* img2img + custom → "image2image.custom"
* edit + z-image → "edit.z-image"
*/
function buildPresetNotation(mode, modelId) {
const modeMap = {
txt2img: "text2image",
img2img: "image2image",
edit: "edit",
txt2vid: "text2video",
img2vid: "image2video",
};
return `${modeMap[mode]}.${modelId}`;
}
/**
* Get all available mode+model combinations from Custom Configs
* Returns array of tuples [mode, modelId]
* Used for error handling to show what's available
*/
function getAvailableCustomCombinations() {
const cache = getPresetsCacheOrNull();
if (!cache)
return [];
const combinations = [];
for (const preset of cache.values()) {
// Map internal mode back to user-facing mode
const userMode = preset.mode === "txt2img"
? "text2image"
: preset.mode === "img2img"
? "image2image"
: preset.mode === "txt2vid"
? "text2video"
: preset.mode === "img2vid"
? "image2video"
: "edit";
combinations.push([userMode, preset.modelId]);
}
return combinations;
}
/**
* Return the Draw Things custom preset names that explicitly reference a given model filename.
*
* NOTE: Many presets do not set `model` explicitly; those cannot be matched here.
* This is intended for optional, display-only enrichment.
*/
function getCustomPresetLabelsUsingModelFilename(modelFilenameOrPath) {
const cache = getPresetsCacheOrNull();
if (!cache)
return [];
if (typeof modelFilenameOrPath !== "string")
return [];
const wanted = path_1.default.basename(modelFilenameOrPath).trim().toLowerCase();
if (!wanted)
return [];
const labels = new Set();
for (const preset of cache.values()) {
const presetModel = preset.params?.model;
if (typeof presetModel !== "string")
continue;
const presetBase = path_1.default.basename(presetModel).trim().toLowerCase();
if (presetBase === wanted) {
labels.add(preset.name);
}
}
return Array.from(labels).sort();
}