@stackmemoryai/stackmemory
Version:
Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.
399 lines (398 loc) • 11 kB
JavaScript
import { fileURLToPath as __fileURLToPath } from 'url';
import { dirname as __pathDirname } from 'path';
const __filename = __fileURLToPath(import.meta.url);
const __dirname = __pathDirname(__filename);
import * as fs from "fs";
import * as path from "path";
import * as yaml from "js-yaml";
import {
DEFAULT_CONFIG,
PRESET_PROFILES
} from "./types.js";
class ConfigManager {
static instance = null;
config;
configPath;
fileWatcher;
onChangeCallbacks = [];
constructor(configPath) {
this.configPath = configPath || path.join(process.cwd(), ".stackmemory", "config.yaml");
this.config = this.loadConfig();
}
/**
* Get singleton instance of ConfigManager
*/
static getInstance(configPath) {
if (!ConfigManager.instance) {
ConfigManager.instance = new ConfigManager(configPath);
}
return ConfigManager.instance;
}
/**
* Load configuration from file or use defaults
*/
loadConfig() {
try {
if (fs.existsSync(this.configPath)) {
const content = fs.readFileSync(this.configPath, "utf-8");
const loaded = yaml.load(content);
return this.mergeWithDefaults(loaded);
}
} catch (error) {
console.warn(`Failed to load config from ${this.configPath}:`, error);
}
return this.mergeWithDefaults({});
}
/**
* Merge loaded config with defaults
*/
mergeWithDefaults(loaded) {
const config = {
version: loaded.version || DEFAULT_CONFIG.version,
profile: loaded.profile,
scoring: {
weights: {
...DEFAULT_CONFIG.scoring.weights,
...loaded.scoring?.weights
},
tool_scores: {
...DEFAULT_CONFIG.scoring.tool_scores,
...loaded.scoring?.tool_scores
}
},
retention: {
local: {
...DEFAULT_CONFIG.retention.local,
...loaded.retention?.local
},
remote: {
...DEFAULT_CONFIG.retention.remote,
...loaded.retention?.remote
},
generational_gc: {
...DEFAULT_CONFIG.retention.generational_gc,
...loaded.retention?.generational_gc
}
},
performance: { ...DEFAULT_CONFIG.performance, ...loaded.performance },
profiles: { ...PRESET_PROFILES, ...loaded.profiles }
};
if (config.profile && config.profiles?.[config.profile]) {
this.applyProfile(config, config.profiles[config.profile]);
}
return config;
}
/**
* Apply a profile to the configuration
*/
applyProfile(config, profile) {
if (profile.scoring) {
if (profile.scoring.weights) {
config.scoring.weights = {
...config.scoring.weights,
...profile.scoring.weights
};
}
if (profile.scoring.tool_scores) {
config.scoring.tool_scores = {
...config.scoring.tool_scores,
...profile.scoring.tool_scores
};
}
}
if (profile.retention) {
if (profile.retention.local) {
config.retention.local = {
...config.retention.local,
...profile.retention.local
};
}
if (profile.retention.remote) {
config.retention.remote = {
...config.retention.remote,
...profile.retention.remote
};
}
if (profile.retention.generational_gc) {
config.retention.generational_gc = {
...config.retention.generational_gc,
...profile.retention.generational_gc
};
}
}
if (profile.performance) {
config.performance = { ...config.performance, ...profile.performance };
}
}
/**
* Validate configuration
*/
validate() {
const result = {
valid: true,
errors: [],
warnings: [],
suggestions: []
};
const weights = this.config.scoring.weights;
const weightSum = weights.base + weights.impact + weights.persistence + weights.reference;
if (Math.abs(weightSum - 1) > 1e-3) {
result.errors.push(
`Weights must sum to 1.0 (current: ${weightSum.toFixed(3)})`
);
result.valid = false;
}
Object.entries(weights).forEach(([key, value]) => {
if (value < 0 || value > 1) {
result.errors.push(
`Weight ${key} must be between 0 and 1 (current: ${value})`
);
result.valid = false;
}
});
Object.entries(this.config.scoring.tool_scores).forEach(([tool, score]) => {
if (score !== void 0 && (score < 0 || score > 1)) {
result.errors.push(
`Tool score for ${tool} must be between 0 and 1 (current: ${score})`
);
result.valid = false;
}
});
const youngMs = this.parseDuration(this.config.retention.local.young);
const matureMs = this.parseDuration(this.config.retention.local.mature);
const oldMs = this.parseDuration(this.config.retention.local.old);
if (youngMs >= matureMs) {
result.errors.push(
"Young retention period must be less than mature period"
);
result.valid = false;
}
if (matureMs >= oldMs) {
result.errors.push(
"Mature retention period must be less than old period"
);
result.valid = false;
}
const maxSize = this.parseSize(this.config.retention.local.max_size);
const availableSpace = this.getAvailableDiskSpace();
if (availableSpace > 0 && maxSize > availableSpace) {
result.warnings.push(
`max_size (${this.config.retention.local.max_size}) exceeds available disk space`
);
}
if (this.config.performance.retrieval_timeout_ms < 100) {
result.warnings.push(
"retrieval_timeout_ms < 100ms may be too aggressive"
);
}
if (this.config.performance.max_stack_depth > 1e4) {
result.warnings.push("max_stack_depth > 10000 may impact performance");
}
if (!this.config.profile) {
result.suggestions.push("Consider using a profile for your use case");
}
if (this.config?.scoring?.tool_scores?.search && this.config.scoring.tool_scores.search < 0.5) {
result.suggestions.push(
"Search tool score seems low - consider increasing for better discovery"
);
}
return result;
}
/**
* Parse duration string to milliseconds
*/
parseDuration(duration) {
const match = duration.match(/^(\d+)([hdwm])$/);
if (!match) return 0;
const value = parseInt(match[1]);
const unit = match[2];
const multipliers = {
h: 36e5,
// hours
d: 864e5,
// days
w: 6048e5,
// weeks
m: 2592e6
// months (30 days)
};
return value * (multipliers[unit] || 0);
}
/**
* Parse size string to bytes
*/
parseSize(size) {
const match = size.match(/^(\d+(?:\.\d+)?)([KMGT]B)?$/i);
if (!match) return 0;
const value = parseFloat(match[1]);
const unit = match[2]?.toUpperCase() || "B";
const multipliers = {
B: 1,
KB: 1024,
MB: 1024 * 1024,
GB: 1024 * 1024 * 1024,
TB: 1024 * 1024 * 1024 * 1024
};
return value * (multipliers[unit] || 1);
}
/**
* Get available disk space (simplified)
*/
getAvailableDiskSpace() {
return 0;
}
/**
* Save configuration to file
*/
save() {
const dir = path.dirname(this.configPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const content = yaml.dump(this.config, {
indent: 2,
lineWidth: 120,
noRefs: true
});
fs.writeFileSync(this.configPath, content, "utf-8");
}
/**
* Get current configuration
*/
getConfig() {
return { ...this.config };
}
/**
* Get a specific configuration value by path
* Example: config.get('project.id') returns config.project.id
*/
get(path2) {
const keys = path2.split(".");
let value = this.config;
for (const key of keys) {
if (value && typeof value === "object" && key in value) {
value = value[key];
} else {
return void 0;
}
}
return value;
}
/**
* Set active profile
*/
setProfile(profileName) {
const allProfiles = { ...PRESET_PROFILES, ...this.config.profiles };
if (!allProfiles[profileName]) {
return false;
}
this.config.profile = profileName;
this.applyProfile(this.config, allProfiles[profileName]);
this.notifyChange();
return true;
}
/**
* Update weights
*/
updateWeights(weights) {
this.config.scoring.weights = {
...this.config.scoring.weights,
...weights
};
this.notifyChange();
}
/**
* Update tool scores
*/
updateToolScores(scores) {
this.config.scoring.tool_scores = {
...this.config.scoring.tool_scores,
...scores
};
this.notifyChange();
}
/**
* Enable hot reload
*/
enableHotReload() {
if (this.fileWatcher) return;
if (fs.existsSync(this.configPath)) {
this.fileWatcher = fs.watch(this.configPath, (eventType) => {
if (eventType === "change") {
const newConfig = this.loadConfig();
const validation = this.validate();
if (validation.valid) {
this.config = newConfig;
this.notifyChange();
console.log("Configuration reloaded");
} else {
console.error(
"Invalid configuration, keeping previous:",
validation.errors
);
}
}
});
}
}
/**
* Disable hot reload
*/
disableHotReload() {
if (this.fileWatcher) {
this.fileWatcher.close();
this.fileWatcher = void 0;
}
}
/**
* Register change callback
*/
onChange(callback) {
this.onChangeCallbacks.push(callback);
}
/**
* Notify all change callbacks
*/
notifyChange() {
const config = this.getConfig();
this.onChangeCallbacks.forEach((cb) => cb(config));
}
/**
* Calculate importance score for a tool
*/
calculateScore(tool, additionalFactors) {
const baseScore = this.config.scoring.tool_scores[tool] || 0.5;
const weights = this.config.scoring.weights;
let score = baseScore * weights.base;
if (additionalFactors) {
if (additionalFactors.filesAffected !== void 0) {
const impactMultiplier = Math.min(
additionalFactors.filesAffected / 10,
1
);
score += impactMultiplier * weights.impact;
}
if (additionalFactors.isPermanent) {
score += 0.2 * weights.persistence;
}
if (additionalFactors.referenceCount !== void 0) {
const refMultiplier = Math.min(
additionalFactors.referenceCount / 100,
1
);
score += refMultiplier * weights.reference;
}
}
return Math.min(Math.max(score, 0), 1);
}
/**
* Get available profiles
*/
getProfiles() {
return { ...PRESET_PROFILES, ...this.config.profiles };
}
}
export {
ConfigManager
};
//# sourceMappingURL=config-manager.js.map