preferenceStore.tsโข7.78 kB
/**
* User preferences management
*/
import * as fs from "fs/promises";
import * as path from "path";
import { ToolsetConfig } from "../server/tools/toolset/types.js";
import { APP_TECHNICAL_NAME, BRAND_NAME } from "./appConfig.js";
import { getHomeDir } from "../utils/paths.js";
// Configuration directory structure
const BRAND_CONFIG_DIR = path.join(
getHomeDir(),
`.${BRAND_NAME.toLowerCase()}`
);
const APP_CONFIG_DIR = path.join(BRAND_CONFIG_DIR, APP_TECHNICAL_NAME);
const CONFIG_FILE = path.join(APP_CONFIG_DIR, "config.json");
/**
* User preferences structure
*/
export interface UserPreferences {
/** Stored toolset configurations */
toolsets: Record<string, ToolsetConfig>;
/** User's preferred path to their MCP server configuration file */
mcpConfigPath?: string;
/** Name of the last equipped toolset */
lastEquippedToolset?: string;
/** Last updated timestamp */
lastUpdated?: string;
/** Version of preferences format */
version?: string;
}
/**
* Complete configuration structure
*/
export interface CompleteConfig extends UserPreferences {
/** Custom persona directory path (overrides default ~/.toolprint/hypertool-mcp/personas) */
personaDir?: string;
/** Application sync configurations */
applications?: Record<string, any>;
/** Last backup timestamp */
lastBackup?: string;
/** Server-related settings */
serverSettings?: {
/** Maximum number of concurrent server connections */
maxConcurrentConnections?: number;
};
/** Feature flags for experimental functionality */
featureFlags?: {
/** Enable interactive setup wizard on first run (default: disabled) */
setupWizardEnabled?: boolean;
// Future feature flags can be added here
};
}
/**
* Default preferences
*/
const DEFAULT_PREFERENCES: UserPreferences = {
toolsets: {},
version: "1.0.0",
};
/**
* Ensure config directory exists
*/
async function ensureConfigDir(): Promise<void> {
try {
await fs.mkdir(APP_CONFIG_DIR, { recursive: true });
} catch {
// Directory might already exist, that's fine
}
}
/**
* Load complete configuration from config file
*/
export async function loadCompleteConfig(): Promise<CompleteConfig> {
try {
await ensureConfigDir();
const content = await fs.readFile(CONFIG_FILE, "utf-8");
const config = JSON.parse(content) as CompleteConfig;
return config;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
// File doesn't exist, return defaults
return {
...DEFAULT_PREFERENCES,
lastUpdated: new Date().toISOString(),
};
}
throw error;
}
}
/**
* Load user preferences from config file
*/
export async function loadUserPreferences(): Promise<UserPreferences> {
try {
await ensureConfigDir();
const content = await fs.readFile(CONFIG_FILE, "utf-8");
const config = JSON.parse(content) as CompleteConfig;
// Extract preference fields from config
const preferences: UserPreferences = {
toolsets: config.toolsets || {},
mcpConfigPath: config.mcpConfigPath,
lastEquippedToolset: config.lastEquippedToolset,
lastUpdated: config.lastUpdated,
version: config.version,
};
// Ensure all required fields exist
return {
...DEFAULT_PREFERENCES,
...preferences,
lastUpdated: preferences.lastUpdated || new Date().toISOString(),
};
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
// File doesn't exist, create with defaults
const defaultPrefs = {
...DEFAULT_PREFERENCES,
lastUpdated: new Date().toISOString(),
};
await saveUserPreferences(defaultPrefs);
return defaultPrefs;
}
throw error;
}
}
/**
* Save user preferences to config file
*/
export async function saveUserPreferences(
preferences: UserPreferences
): Promise<void> {
await ensureConfigDir();
// Load existing config to preserve non-preference fields
let existingConfig: Partial<CompleteConfig> = {};
try {
const content = await fs.readFile(CONFIG_FILE, "utf-8");
existingConfig = JSON.parse(content) as CompleteConfig;
} catch {
// File doesn't exist or is invalid, use empty object
}
const updatedConfig: CompleteConfig = {
...existingConfig,
...preferences,
lastUpdated: new Date().toISOString(),
};
await fs.writeFile(
CONFIG_FILE,
JSON.stringify(updatedConfig, null, 2),
"utf-8"
);
}
/**
* Update MCP config path preference
*/
export async function updateMcpConfigPath(configPath: string): Promise<void> {
const preferences = await loadUserPreferences();
preferences.mcpConfigPath = configPath;
await saveUserPreferences(preferences);
}
/**
* Get stored toolsets
*/
export async function loadStoredToolsets(): Promise<
Record<string, ToolsetConfig>
> {
const preferences = await loadUserPreferences();
return preferences.toolsets;
}
/**
* Save toolsets
*/
export async function saveStoredToolsets(
toolsets: Record<string, ToolsetConfig>
): Promise<void> {
const preferences = await loadUserPreferences();
preferences.toolsets = toolsets;
await saveUserPreferences(preferences);
}
/**
* Get the last equipped toolset name
*/
export async function getLastEquippedToolset(): Promise<string | undefined> {
const preferences = await loadUserPreferences();
return preferences.lastEquippedToolset;
}
/**
* Save the last equipped toolset name
*/
export async function saveLastEquippedToolset(
toolsetName: string | undefined
): Promise<void> {
const preferences = await loadUserPreferences();
preferences.lastEquippedToolset = toolsetName;
await saveUserPreferences(preferences);
}
/**
* Get a specific feature flag value
*/
export async function getFeatureFlag(
flagName: string
): Promise<boolean | undefined> {
try {
await ensureConfigDir();
const content = await fs.readFile(CONFIG_FILE, "utf-8");
const config = JSON.parse(content) as CompleteConfig;
return config.featureFlags?.[flagName as keyof typeof config.featureFlags];
} catch (error) {
// If config doesn't exist or is invalid, return undefined
return undefined;
}
}
/**
* Set a specific feature flag value
*/
export async function setFeatureFlag(
flagName: string,
value: boolean
): Promise<void> {
await ensureConfigDir();
// Load existing config to preserve all fields
let existingConfig: Partial<CompleteConfig> = {};
try {
const content = await fs.readFile(CONFIG_FILE, "utf-8");
existingConfig = JSON.parse(content) as CompleteConfig;
} catch {
// File doesn't exist or is invalid, use empty object
}
// Ensure featureFlags section exists
if (!existingConfig.featureFlags) {
existingConfig.featureFlags = {};
}
// Set the specific flag
(existingConfig.featureFlags as any)[flagName] = value;
existingConfig.lastUpdated = new Date().toISOString();
await fs.writeFile(
CONFIG_FILE,
JSON.stringify(existingConfig, null, 2),
"utf-8"
);
}
/**
* Get all feature flags
*/
export async function getFeatureFlags(): Promise<
Record<string, boolean> | undefined
> {
try {
await ensureConfigDir();
const content = await fs.readFile(CONFIG_FILE, "utf-8");
const config = JSON.parse(content) as CompleteConfig;
return config.featureFlags as Record<string, boolean> | undefined;
} catch (error) {
// If config doesn't exist or is invalid, return undefined
return undefined;
}
}
/**
* Get paths for configuration files
*/
export function getConfigPaths() {
return {
brandDir: BRAND_CONFIG_DIR,
appDir: APP_CONFIG_DIR,
configFile: CONFIG_FILE,
preferencesFile: CONFIG_FILE, // Deprecated: use configFile instead
};
}