Files
Mastermind/src/storage.js
T
Илья Глазунов 06e178762d
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
Rename project from "Cheating Daddy" to "Mastermind" across all files, update version to 0.6.0, and implement migration functionality for users with existing configurations. Enhance onboarding experience with migration options and update relevant documentation.
2026-01-16 01:16:43 +03:00

570 lines
16 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const os = require('os');
const CONFIG_VERSION = 1;
// Default values
const DEFAULT_CONFIG = {
configVersion: CONFIG_VERSION,
onboarded: false,
layout: 'normal',
};
const DEFAULT_CREDENTIALS = {
apiKey: '',
// OpenAI Realtime API settings
openaiApiKey: '',
openaiBaseUrl: '',
openaiModel: 'gpt-4o-realtime-preview-2024-12-17',
// OpenAI SDK settings (for BotHub and other providers)
openaiSdkApiKey: '',
openaiSdkBaseUrl: '',
openaiSdkModel: 'gpt-4o',
openaiSdkVisionModel: 'gpt-4o',
openaiSdkWhisperModel: 'whisper-1',
};
const DEFAULT_PREFERENCES = {
customPrompt: '',
selectedProfile: 'interview',
selectedLanguage: 'en-US',
selectedScreenshotInterval: '5',
selectedImageQuality: 'medium',
advancedMode: false,
audioMode: 'speaker_only',
audioInputMode: 'auto',
fontSize: 'medium',
backgroundTransparency: 0.8,
googleSearchEnabled: false,
aiProvider: 'gemini',
};
const DEFAULT_KEYBINDS = null; // null means use system defaults
const DEFAULT_LIMITS = {
data: [], // Array of { date: 'YYYY-MM-DD', flash: { count: 0 }, flashLite: { count: 0 } }
};
// Get the config directory path based on OS
function getConfigDir() {
const platform = os.platform();
let configDir;
if (platform === 'win32') {
configDir = path.join(os.homedir(), 'AppData', 'Roaming', 'mastermind-config');
} else if (platform === 'darwin') {
configDir = path.join(os.homedir(), 'Library', 'Application Support', 'mastermind-config');
} else {
configDir = path.join(os.homedir(), '.config', 'mastermind-config');
}
return configDir;
}
// Get the old config directory path for migration
function getOldConfigDir() {
const platform = os.platform();
let configDir;
if (platform === 'win32') {
configDir = path.join(os.homedir(), 'AppData', 'Roaming', 'cheating-daddy-config');
} else if (platform === 'darwin') {
configDir = path.join(os.homedir(), 'Library', 'Application Support', 'cheating-daddy-config');
} else {
configDir = path.join(os.homedir(), '.config', 'cheating-daddy-config');
}
return configDir;
}
// Check if old config directory exists
function hasOldConfig() {
const oldDir = getOldConfigDir();
return fs.existsSync(oldDir);
}
// Migrate config from old directory to new directory if needed
function migrateFromOldConfig() {
const oldDir = getOldConfigDir();
const newDir = getConfigDir();
if (!fs.existsSync(oldDir)) {
console.log('No old config found to migrate');
return false;
}
if (fs.existsSync(newDir)) {
// NOTE: Does not matter if the new config directory already exists, we will overwrite it with the old config
fs.rmSync(newDir, { recursive: true, force: true });
console.log('New config directory already exists, overwriting with old config');
}
console.log(`Migrating config from ${oldDir} to ${newDir}...`);
try {
const parentDir = path.dirname(newDir);
if (!fs.existsSync(parentDir)) {
fs.mkdirSync(parentDir, { recursive: true });
}
fs.renameSync(oldDir, newDir);
console.log('Migration successful');
return true;
} catch (error) {
console.error('Migration failed:', error.message);
return false;
}
}
// File paths
function getConfigPath() {
return path.join(getConfigDir(), 'config.json');
}
function getCredentialsPath() {
return path.join(getConfigDir(), 'credentials.json');
}
function getPreferencesPath() {
return path.join(getConfigDir(), 'preferences.json');
}
function getKeybindsPath() {
return path.join(getConfigDir(), 'keybinds.json');
}
function getLimitsPath() {
return path.join(getConfigDir(), 'limits.json');
}
function getHistoryDir() {
return path.join(getConfigDir(), 'history');
}
// Helper to read JSON file safely
function readJsonFile(filePath, defaultValue) {
try {
if (fs.existsSync(filePath)) {
const data = fs.readFileSync(filePath, 'utf8');
return JSON.parse(data);
}
} catch (error) {
console.warn(`Error reading ${filePath}:`, error.message);
}
return defaultValue;
}
// Helper to write JSON file safely
function writeJsonFile(filePath, data) {
try {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
return true;
} catch (error) {
console.error(`Error writing ${filePath}:`, error.message);
return false;
}
}
// Check if we need to reset (no configVersion or wrong version)
function needsReset() {
const configPath = getConfigPath();
if (!fs.existsSync(configPath)) {
return true;
}
try {
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
return !config.configVersion || config.configVersion !== CONFIG_VERSION;
} catch {
return true;
}
}
// Wipe and reinitialize the config directory
function resetConfigDir() {
const configDir = getConfigDir();
console.log('Resetting config directory...');
// Remove existing directory if it exists
if (fs.existsSync(configDir)) {
fs.rmSync(configDir, { recursive: true, force: true });
}
// Create fresh directory structure
fs.mkdirSync(configDir, { recursive: true });
fs.mkdirSync(getHistoryDir(), { recursive: true });
// Initialize with defaults
writeJsonFile(getConfigPath(), DEFAULT_CONFIG);
writeJsonFile(getCredentialsPath(), DEFAULT_CREDENTIALS);
writeJsonFile(getPreferencesPath(), DEFAULT_PREFERENCES);
console.log('Config directory initialized with defaults');
}
// Initialize storage - call this on app startup
function initializeStorage() {
if (needsReset()) {
resetConfigDir();
} else {
// Ensure history directory exists
const historyDir = getHistoryDir();
if (!fs.existsSync(historyDir)) {
fs.mkdirSync(historyDir, { recursive: true });
}
}
}
// ============ CONFIG ============
function getConfig() {
return readJsonFile(getConfigPath(), DEFAULT_CONFIG);
}
function setConfig(config) {
const current = getConfig();
const updated = { ...current, ...config, configVersion: CONFIG_VERSION };
return writeJsonFile(getConfigPath(), updated);
}
function updateConfig(key, value) {
const config = getConfig();
config[key] = value;
return writeJsonFile(getConfigPath(), config);
}
// ============ CREDENTIALS ============
function getCredentials() {
return readJsonFile(getCredentialsPath(), DEFAULT_CREDENTIALS);
}
function setCredentials(credentials) {
const current = getCredentials();
const updated = { ...current, ...credentials };
return writeJsonFile(getCredentialsPath(), updated);
}
function getApiKey() {
return getCredentials().apiKey || '';
}
function setApiKey(apiKey) {
return setCredentials({ apiKey });
}
function getOpenAICredentials() {
const creds = getCredentials();
return {
apiKey: creds.openaiApiKey || '',
baseUrl: creds.openaiBaseUrl || '',
model: creds.openaiModel || 'gpt-4o-realtime-preview-2024-12-17',
};
}
function setOpenAICredentials(config) {
const updates = {};
if (config.apiKey !== undefined) updates.openaiApiKey = config.apiKey;
if (config.baseUrl !== undefined) updates.openaiBaseUrl = config.baseUrl;
if (config.model !== undefined) updates.openaiModel = config.model;
return setCredentials(updates);
}
function getOpenAISDKCredentials() {
const creds = getCredentials();
return {
apiKey: creds.openaiSdkApiKey || '',
baseUrl: creds.openaiSdkBaseUrl || '',
model: creds.openaiSdkModel || 'gpt-4o',
visionModel: creds.openaiSdkVisionModel || 'gpt-4o',
whisperModel: creds.openaiSdkWhisperModel || 'whisper-1',
};
}
function setOpenAISDKCredentials(config) {
const updates = {};
if (config.apiKey !== undefined) updates.openaiSdkApiKey = config.apiKey;
if (config.baseUrl !== undefined) updates.openaiSdkBaseUrl = config.baseUrl;
if (config.model !== undefined) updates.openaiSdkModel = config.model;
if (config.visionModel !== undefined) updates.openaiSdkVisionModel = config.visionModel;
if (config.whisperModel !== undefined) updates.openaiSdkWhisperModel = config.whisperModel;
return setCredentials(updates);
}
// ============ PREFERENCES ============
function getPreferences() {
const saved = readJsonFile(getPreferencesPath(), {});
return { ...DEFAULT_PREFERENCES, ...saved };
}
function setPreferences(preferences) {
const current = getPreferences();
const updated = { ...current, ...preferences };
return writeJsonFile(getPreferencesPath(), updated);
}
function updatePreference(key, value) {
const preferences = getPreferences();
preferences[key] = value;
return writeJsonFile(getPreferencesPath(), preferences);
}
// ============ KEYBINDS ============
function getKeybinds() {
return readJsonFile(getKeybindsPath(), DEFAULT_KEYBINDS);
}
function setKeybinds(keybinds) {
return writeJsonFile(getKeybindsPath(), keybinds);
}
// ============ LIMITS (Rate Limiting) ============
function getLimits() {
return readJsonFile(getLimitsPath(), DEFAULT_LIMITS);
}
function setLimits(limits) {
return writeJsonFile(getLimitsPath(), limits);
}
function getTodayDateString() {
const now = new Date();
return now.toISOString().split('T')[0]; // YYYY-MM-DD
}
function getTodayLimits() {
const limits = getLimits();
const today = getTodayDateString();
// Find today's entry
const todayEntry = limits.data.find(entry => entry.date === today);
if (todayEntry) {
return todayEntry;
}
// No entry for today - clean old entries and create new one
limits.data = limits.data.filter(entry => entry.date === today);
const newEntry = {
date: today,
flash: { count: 0 },
flashLite: { count: 0 },
};
limits.data.push(newEntry);
setLimits(limits);
return newEntry;
}
function incrementLimitCount(model) {
const limits = getLimits();
const today = getTodayDateString();
// Find or create today's entry
let todayEntry = limits.data.find(entry => entry.date === today);
if (!todayEntry) {
// Clean old entries and create new one
limits.data = [];
todayEntry = {
date: today,
flash: { count: 0 },
flashLite: { count: 0 },
};
limits.data.push(todayEntry);
} else {
// Clean old entries, keep only today
limits.data = limits.data.filter(entry => entry.date === today);
}
// Increment the appropriate model count
if (model === 'gemini-2.5-flash') {
todayEntry.flash.count++;
} else if (model === 'gemini-2.5-flash-lite') {
todayEntry.flashLite.count++;
}
setLimits(limits);
return todayEntry;
}
function getAvailableModel() {
const todayLimits = getTodayLimits();
// RPD limits: flash = 20, flash-lite = 20
// After both exhausted, fall back to flash (for paid API users)
if (todayLimits.flash.count < 20) {
return 'gemini-2.5-flash';
} else if (todayLimits.flashLite.count < 20) {
return 'gemini-2.5-flash-lite';
}
return 'gemini-2.5-flash'; // Default to flash for paid API users
}
// ============ HISTORY ============
function getSessionPath(sessionId) {
return path.join(getHistoryDir(), `${sessionId}.json`);
}
function saveSession(sessionId, data) {
const sessionPath = getSessionPath(sessionId);
// Load existing session to preserve metadata
const existingSession = readJsonFile(sessionPath, null);
const sessionData = {
sessionId,
createdAt: existingSession?.createdAt || parseInt(sessionId),
lastUpdated: Date.now(),
// Profile context - set once when session starts
profile: data.profile || existingSession?.profile || null,
customPrompt: data.customPrompt || existingSession?.customPrompt || null,
// Conversation data
conversationHistory: data.conversationHistory || existingSession?.conversationHistory || [],
screenAnalysisHistory: data.screenAnalysisHistory || existingSession?.screenAnalysisHistory || [],
};
return writeJsonFile(sessionPath, sessionData);
}
function getSession(sessionId) {
return readJsonFile(getSessionPath(sessionId), null);
}
function getAllSessions() {
const historyDir = getHistoryDir();
try {
if (!fs.existsSync(historyDir)) {
return [];
}
const files = fs
.readdirSync(historyDir)
.filter(f => f.endsWith('.json'))
.sort((a, b) => {
// Sort by timestamp descending (newest first)
const tsA = parseInt(a.replace('.json', ''));
const tsB = parseInt(b.replace('.json', ''));
return tsB - tsA;
});
return files
.map(file => {
const sessionId = file.replace('.json', '');
const data = readJsonFile(path.join(historyDir, file), null);
if (data) {
return {
sessionId,
createdAt: data.createdAt,
lastUpdated: data.lastUpdated,
messageCount: data.conversationHistory?.length || 0,
screenAnalysisCount: data.screenAnalysisHistory?.length || 0,
profile: data.profile || null,
customPrompt: data.customPrompt || null,
};
}
return null;
})
.filter(Boolean);
} catch (error) {
console.error('Error reading sessions:', error.message);
return [];
}
}
function deleteSession(sessionId) {
const sessionPath = getSessionPath(sessionId);
try {
if (fs.existsSync(sessionPath)) {
fs.unlinkSync(sessionPath);
return true;
}
} catch (error) {
console.error('Error deleting session:', error.message);
}
return false;
}
function deleteAllSessions() {
const historyDir = getHistoryDir();
try {
if (fs.existsSync(historyDir)) {
const files = fs.readdirSync(historyDir).filter(f => f.endsWith('.json'));
files.forEach(file => {
fs.unlinkSync(path.join(historyDir, file));
});
}
return true;
} catch (error) {
console.error('Error deleting all sessions:', error.message);
return false;
}
}
// ============ CLEAR ALL DATA ============
function clearAllData() {
resetConfigDir();
return true;
}
module.exports = {
// Initialization
initializeStorage,
getConfigDir,
// Migration
hasOldConfig,
migrateFromOldConfig,
// Config
getConfig,
setConfig,
updateConfig,
// Credentials
getCredentials,
setCredentials,
getApiKey,
setApiKey,
getOpenAICredentials,
setOpenAICredentials,
getOpenAISDKCredentials,
setOpenAISDKCredentials,
// Preferences
getPreferences,
setPreferences,
updatePreference,
// Keybinds
getKeybinds,
setKeybinds,
// Limits (Rate Limiting)
getLimits,
setLimits,
getTodayLimits,
incrementLimitCount,
getAvailableModel,
// History
saveSession,
getSession,
getAllSessions,
deleteSession,
deleteAllSessions,
// Clear all
clearAllData,
};