sf-agent-framework
Version:
AI Agent Orchestration Framework for Salesforce Development - Two-phase architecture with 70% context reduction
583 lines (501 loc) • 16.4 kB
JavaScript
/**
* Update-Safe Configuration Manager
*
* Purpose: Preserve user customizations through framework version updates
* Ensures user settings, agent overrides, and preferences persist across upgrades
*
* @module UpdateSafeConfig
* @version 1.0.0
* @date 2025-11-25
*/
const fs = require('fs-extra');
const path = require('path');
const yaml = require('js-yaml');
class UpdateSafeConfig {
constructor(rootDir = process.cwd()) {
this.rootDir = rootDir;
this.userConfigDir = path.join(rootDir, '.sf-agent');
this.frameworkConfigDir = path.join(rootDir, 'sf-core', 'config');
// User-specific paths (never overwritten)
this.userConfigPath = path.join(this.userConfigDir, 'user-config.json');
this.userPreferencesPath = path.join(this.userConfigDir, 'preferences.json');
this.agentOverridesDir = path.join(this.userConfigDir, 'agent-overrides');
this.workflowOverridesDir = path.join(this.userConfigDir, 'workflow-overrides');
}
/**
* Initialize user configuration directory structure
*/
async initialize() {
try {
// Create .sf-agent directory structure
await fs.ensureDir(this.userConfigDir);
await fs.ensureDir(this.agentOverridesDir);
await fs.ensureDir(this.workflowOverridesDir);
// Create default user config if doesn't exist
if (!(await fs.pathExists(this.userConfigPath))) {
await this.createDefaultUserConfig();
}
// Create default preferences if doesn't exist
if (!(await fs.pathExists(this.userPreferencesPath))) {
await this.createDefaultPreferences();
}
console.log('✓ Update-safe configuration initialized');
return true;
} catch (error) {
console.error('✗ Failed to initialize update-safe config:', error.message);
return false;
}
}
/**
* Create default user configuration
*/
async createDefaultUserConfig() {
const defaultConfig = {
version: '1.0.0',
created: new Date().toISOString(),
lastUpdated: new Date().toISOString(),
customizations: {
agents: {},
workflows: {},
templates: {},
phases: {},
},
overrides: {
agentDirectory: this.agentOverridesDir,
workflowDirectory: this.workflowOverridesDir,
enabled: true,
},
metadata: {
frameworkVersion: '4.5.0',
compatibilityVersion: '4.5.0',
},
};
await fs.writeJson(this.userConfigPath, defaultConfig, { spaces: 2 });
console.log('✓ Created default user configuration');
}
/**
* Create default user preferences
*/
async createDefaultPreferences() {
const defaultPreferences = {
version: '1.0.0',
display: {
verboseOutput: false,
showBenchmarks: true,
coloredOutput: true,
showProgress: true,
},
workflow: {
autoSelectComplexity: true,
confirmBeforeExecution: false,
saveWorkflowHistory: true,
defaultTrack: 'balanced', // quick | balanced | enterprise
},
agents: {
defaultCommunicationStyle: 'balanced', // concise | balanced | detailed
defaultExpertiseLevel: 'senior', // junior | mid | senior
autoReflection: true,
cachePersonas: true,
},
models: {
preferredProvider: 'auto', // auto | anthropic | openai | google
preferredModelCoding: 'claude_opus_4_5',
preferredModelArchitecture: 'claude_opus_4_5',
preferredModelDaily: 'gpt_5_1_instant',
enablePromptCaching: true,
costOptimization: true,
},
context: {
enableDocumentSharding: true,
maxContextUsage: 0.8, // 80% of available context
priorityLoadingEnabled: true,
},
notifications: {
showCompletionNotifications: true,
showErrorNotifications: true,
soundEnabled: false,
},
};
await fs.writeJson(this.userPreferencesPath, defaultPreferences, { spaces: 2 });
console.log('✓ Created default user preferences');
}
/**
* Load user configuration
*/
async loadUserConfig() {
try {
if (await fs.pathExists(this.userConfigPath)) {
return await fs.readJson(this.userConfigPath);
}
return null;
} catch (error) {
console.error('Error loading user config:', error.message);
return null;
}
}
/**
* Load user preferences
*/
async loadPreferences() {
try {
if (await fs.pathExists(this.userPreferencesPath)) {
return await fs.readJson(this.userPreferencesPath);
}
return null;
} catch (error) {
console.error('Error loading preferences:', error.message);
return null;
}
}
/**
* Save user configuration
*/
async saveUserConfig(config) {
try {
config.lastUpdated = new Date().toISOString();
await fs.writeJson(this.userConfigPath, config, { spaces: 2 });
return true;
} catch (error) {
console.error('Error saving user config:', error.message);
return false;
}
}
/**
* Save user preferences
*/
async savePreferences(preferences) {
try {
await fs.writeJson(this.userPreferencesPath, preferences, { spaces: 2 });
return true;
} catch (error) {
console.error('Error saving preferences:', error.message);
return false;
}
}
/**
* Load framework configuration
*/
async loadFrameworkConfig(configName = 'core-config.yaml') {
try {
const configPath = path.join(this.frameworkConfigDir, configName);
if (await fs.pathExists(configPath)) {
const content = await fs.readFile(configPath, 'utf8');
return yaml.load(content);
}
return null;
} catch (error) {
console.error('Error loading framework config:', error.message);
return null;
}
}
/**
* Merge user config with framework config
* User settings always take precedence
*/
async mergeConfigs() {
try {
const userConfig = await this.loadUserConfig();
const frameworkConfig = await this.loadFrameworkConfig();
const preferences = await this.loadPreferences();
if (!frameworkConfig) {
throw new Error('Framework configuration not found');
}
// Start with framework config as base
const merged = { ...frameworkConfig };
// Apply user customizations if they exist
if (userConfig && userConfig.customizations) {
// Merge agent customizations
if (userConfig.customizations.agents) {
merged.agents = this.mergeAgentConfigs(
frameworkConfig.agents,
userConfig.customizations.agents
);
}
// Merge workflow customizations
if (userConfig.customizations.workflows) {
merged.workflows = {
...frameworkConfig.workflows,
...userConfig.customizations.workflows,
};
}
// Merge phase customizations
if (userConfig.customizations.phases) {
merged.phases = this.deepMerge(frameworkConfig.phases, userConfig.customizations.phases);
}
}
// Apply preferences
if (preferences) {
merged.preferences = preferences;
}
// Load agent overrides
merged.agentOverrides = await this.loadAgentOverrides();
return merged;
} catch (error) {
console.error('Error merging configs:', error.message);
throw error;
}
}
/**
* Merge agent configurations
*/
mergeAgentConfigs(frameworkAgents, userAgents) {
const merged = { ...frameworkAgents };
for (const [agentId, userSettings] of Object.entries(userAgents)) {
if (merged[agentId]) {
merged[agentId] = {
...merged[agentId],
...userSettings,
customized: true,
customizedAt: new Date().toISOString(),
};
}
}
return merged;
}
/**
* Load agent override files
*/
async loadAgentOverrides() {
try {
const overrides = {};
if (await fs.pathExists(this.agentOverridesDir)) {
const files = await fs.readdir(this.agentOverridesDir);
for (const file of files) {
if (file.endsWith('.md') || file.endsWith('.yaml')) {
const agentId = path.basename(file, path.extname(file));
const filePath = path.join(this.agentOverridesDir, file);
const content = await fs.readFile(filePath, 'utf8');
overrides[agentId] = {
source: 'user-override',
path: filePath,
content: content,
lastModified: (await fs.stat(filePath)).mtime,
};
}
}
}
return overrides;
} catch (error) {
console.error('Error loading agent overrides:', error.message);
return {};
}
}
/**
* Deep merge objects
*/
deepMerge(target, source) {
const output = { ...target };
if (this.isObject(target) && this.isObject(source)) {
Object.keys(source).forEach((key) => {
if (this.isObject(source[key])) {
if (!(key in target)) {
output[key] = source[key];
} else {
output[key] = this.deepMerge(target[key], source[key]);
}
} else {
output[key] = source[key];
}
});
}
return output;
}
/**
* Check if value is an object
*/
isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item);
}
/**
* Backup user configuration before upgrade
*/
async backupUserConfig() {
try {
const timestamp = new Date().toISOString().replace(/:/g, '-');
const backupDir = path.join(this.userConfigDir, 'backups', timestamp);
await fs.ensureDir(backupDir);
// Backup user config
if (await fs.pathExists(this.userConfigPath)) {
await fs.copy(this.userConfigPath, path.join(backupDir, 'user-config.json'));
}
// Backup preferences
if (await fs.pathExists(this.userPreferencesPath)) {
await fs.copy(this.userPreferencesPath, path.join(backupDir, 'preferences.json'));
}
// Backup agent overrides
if (await fs.pathExists(this.agentOverridesDir)) {
await fs.copy(this.agentOverridesDir, path.join(backupDir, 'agent-overrides'));
}
console.log(`✓ User configuration backed up to: ${backupDir}`);
return backupDir;
} catch (error) {
console.error('Error backing up user config:', error.message);
throw error;
}
}
/**
* Preserve user settings during framework upgrade
*/
async preserveUserSettings(newFrameworkVersion) {
try {
console.log('\n🔄 Preserving user settings during upgrade...');
// 1. Backup current settings
const backupDir = await this.backupUserConfig();
console.log(' ✓ User settings backed up');
// 2. Load current user settings
const currentUserConfig = await this.loadUserConfig();
const currentPreferences = await this.loadPreferences();
// 3. Perform framework upgrade (this would be called externally)
console.log(' ⏳ Framework upgrade in progress...');
// 4. Update metadata
if (currentUserConfig) {
currentUserConfig.metadata.previousFrameworkVersion =
currentUserConfig.metadata.frameworkVersion;
currentUserConfig.metadata.frameworkVersion = newFrameworkVersion;
currentUserConfig.metadata.upgradedAt = new Date().toISOString();
await this.saveUserConfig(currentUserConfig);
}
console.log(' ✓ User settings preserved');
console.log(` ✓ Backup available at: ${backupDir}`);
return {
success: true,
backupLocation: backupDir,
version: newFrameworkVersion,
};
} catch (error) {
console.error('✗ Error preserving user settings:', error.message);
throw error;
}
}
/**
* Create agent override file
*/
async createAgentOverride(agentId, content) {
try {
const filePath = path.join(this.agentOverridesDir, `${agentId}.md`);
await fs.writeFile(filePath, content, 'utf8');
// Update user config
const userConfig = await this.loadUserConfig();
if (!userConfig.customizations.agents[agentId]) {
userConfig.customizations.agents[agentId] = {};
}
userConfig.customizations.agents[agentId].hasOverride = true;
userConfig.customizations.agents[agentId].overridePath = filePath;
await this.saveUserConfig(userConfig);
console.log(`✓ Created agent override: ${agentId}`);
return true;
} catch (error) {
console.error('Error creating agent override:', error.message);
return false;
}
}
/**
* Update preference value
*/
async updatePreference(category, key, value) {
try {
const preferences = await this.loadPreferences();
if (preferences[category]) {
preferences[category][key] = value;
await this.savePreferences(preferences);
console.log(`✓ Updated preference: ${category}.${key} = ${value}`);
return true;
} else {
console.error(`✗ Invalid preference category: ${category}`);
return false;
}
} catch (error) {
console.error('Error updating preference:', error.message);
return false;
}
}
/**
* Get effective configuration (merged)
*/
async getEffectiveConfig() {
return await this.mergeConfigs();
}
/**
* Validate user configuration
*/
async validateUserConfig() {
try {
const userConfig = await this.loadUserConfig();
const preferences = await this.loadPreferences();
const validation = {
valid: true,
issues: [],
warnings: [],
};
// Check version compatibility
if (userConfig && userConfig.metadata.frameworkVersion) {
const frameworkVersion = userConfig.metadata.frameworkVersion;
const currentVersion = '4.5.0';
if (frameworkVersion !== currentVersion) {
validation.warnings.push(
`Configuration was created for framework v${frameworkVersion}, ` +
`current version is v${currentVersion}`
);
}
}
// Validate agent overrides exist
if (userConfig && userConfig.customizations.agents) {
for (const [agentId, settings] of Object.entries(userConfig.customizations.agents)) {
if (settings.hasOverride) {
const overridePath = path.join(this.agentOverridesDir, `${agentId}.md`);
if (!(await fs.pathExists(overridePath))) {
validation.issues.push(`Agent override file missing: ${agentId}.md`);
validation.valid = false;
}
}
}
}
// Validate preferences structure
if (preferences) {
const requiredCategories = ['display', 'workflow', 'agents', 'models', 'context'];
for (const category of requiredCategories) {
if (!preferences[category]) {
validation.warnings.push(`Missing preference category: ${category}`);
}
}
}
return validation;
} catch (error) {
return {
valid: false,
issues: [error.message],
warnings: [],
};
}
}
/**
* Reset user configuration to defaults
*/
async resetToDefaults(options = { backupFirst: true }) {
try {
console.log('\n⚠️ Resetting user configuration to defaults...');
// Backup if requested
if (options.backupFirst) {
await this.backupUserConfig();
console.log(' ✓ Current configuration backed up');
}
// Remove current config files
if (await fs.pathExists(this.userConfigPath)) {
await fs.remove(this.userConfigPath);
}
if (await fs.pathExists(this.userPreferencesPath)) {
await fs.remove(this.userPreferencesPath);
}
// Recreate defaults
await this.createDefaultUserConfig();
await this.createDefaultPreferences();
console.log(' ✓ Configuration reset to defaults');
console.log('\n💡 Tip: Agent overrides were NOT removed');
return true;
} catch (error) {
console.error('✗ Error resetting configuration:', error.message);
return false;
}
}
}
module.exports = UpdateSafeConfig;