@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.
446 lines (444 loc) • 17.2 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 { Command } from "commander";
import * as fs from "fs";
import * as path from "path";
import * as yaml from "js-yaml";
import chalk from "chalk";
import { ConfigManager } from "../../core/config/config-manager.js";
import {
DEFAULT_CONFIG,
DEFAULT_WEIGHTS,
DEFAULT_TOOL_SCORES
} from "../../core/config/types.js";
import {
loadStorageConfig,
enableChromaDB,
disableChromaDB,
getStorageModeDescription
} from "../../core/config/storage-config.js";
import inquirer from "inquirer";
function createConfigCommand() {
const config = new Command("config").description(
"Manage StackMemory configuration"
);
config.command("validate").description("Validate configuration file").option(
"-f, --file <path>",
"Path to config file",
".stackmemory/config.yaml"
).option("--fix", "Attempt to auto-fix common issues").action(async (options) => {
console.log(chalk.blue("\u{1F50D} Validating configuration..."));
const configPath = path.resolve(options.file);
const manager = new ConfigManager(configPath);
const result = manager.validate();
if (result.errors.length > 0) {
console.log(chalk.red("\n\u2717 Errors:"));
result.errors.forEach((error) => {
console.log(chalk.red(` \u2022 ${error}`));
});
}
if (result.warnings.length > 0) {
console.log(chalk.yellow("\n\u26A0 Warnings:"));
result.warnings.forEach((warning) => {
console.log(chalk.yellow(` \u2022 ${warning}`));
});
}
if (result.suggestions.length > 0) {
console.log(chalk.cyan("\n\u{1F4A1} Suggestions:"));
result.suggestions.forEach((suggestion) => {
console.log(chalk.cyan(` \u2022 ${suggestion}`));
});
}
if (options.fix && result.errors.length > 0) {
console.log(chalk.blue("\n\u{1F527} Attempting auto-fix..."));
const config2 = manager.getConfig();
const weights = config2.scoring.weights;
const weightSum = weights.base + weights.impact + weights.persistence + weights.reference;
if (Math.abs(weightSum - 1) > 1e-3) {
const factor = 1 / weightSum;
manager.updateWeights({
base: weights.base * factor,
impact: weights.impact * factor,
persistence: weights.persistence * factor,
reference: weights.reference * factor
});
manager.save();
console.log(chalk.green(" \u2713 Normalized weights to sum to 1.0"));
}
}
if (result.valid) {
console.log(chalk.green("\n\u2705 Configuration is valid"));
process.exit(0);
} else {
console.log(chalk.red("\n\u274C Configuration has errors"));
process.exit(1);
}
});
config.command("init").description("Initialize configuration file with defaults").option("-p, --profile <name>", "Use a preset profile", "default").option("-f, --force", "Overwrite existing config").action(async (options) => {
const configPath = path.join(
process.cwd(),
".stackmemory",
"config.yaml"
);
if (fs.existsSync(configPath) && !options.force) {
console.log(
chalk.yellow(
"\u26A0 Config file already exists. Use --force to overwrite."
)
);
process.exit(1);
}
const dir = path.dirname(configPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const config2 = { ...DEFAULT_CONFIG };
if (options.profile && options.profile !== "default") {
config2.profile = options.profile;
}
const content = yaml.dump(config2, {
indent: 2,
lineWidth: 120,
noRefs: true
});
fs.writeFileSync(configPath, content, "utf-8");
console.log(chalk.green(`\u2705 Created config file at ${configPath}`));
if (options.profile !== "default") {
console.log(chalk.cyan(`\u{1F4CB} Using profile: ${options.profile}`));
}
});
config.command("show").description("Show current configuration").option("-p, --profile <name>", "Show specific profile").action(async (options) => {
const manager = new ConfigManager();
const config2 = manager.getConfig();
if (options.profile) {
const profiles = manager.getProfiles();
const profile = profiles[options.profile];
if (!profile) {
console.log(chalk.red(`\u274C Profile '${options.profile}' not found`));
console.log(chalk.cyan("Available profiles:"));
Object.keys(profiles).forEach((name) => {
console.log(` \u2022 ${name}`);
});
process.exit(1);
}
console.log(chalk.blue(`
\u{1F4CB} Profile: ${profile.name}`));
if (profile.description) {
console.log(chalk.gray(` ${profile.description}`));
}
console.log("\n" + yaml.dump(profile, { indent: 2 }));
} else {
console.log(chalk.blue("\n\u{1F4CB} Current Configuration:"));
if (config2.profile) {
console.log(chalk.cyan(` Active Profile: ${config2.profile}`));
}
console.log("\n" + yaml.dump(config2, { indent: 2 }));
}
});
config.command("set-profile <name>").description("Set active profile").action(async (name) => {
const manager = new ConfigManager();
if (manager.setProfile(name)) {
manager.save();
console.log(chalk.green(`\u2705 Active profile set to: ${name}`));
} else {
console.log(chalk.red(`\u274C Profile '${name}' not found`));
console.log(chalk.cyan("Available profiles:"));
Object.keys(manager.getProfiles()).forEach((profile) => {
console.log(` \u2022 ${profile}`);
});
process.exit(1);
}
});
config.command("list-profiles").description("List available profiles").action(async () => {
const manager = new ConfigManager();
const profiles = manager.getProfiles();
const currentProfile = manager.getConfig().profile;
console.log(chalk.blue("\n\u{1F4CB} Available Profiles:"));
Object.entries(profiles).forEach(([name, profile]) => {
const marker = name === currentProfile ? chalk.green(" \u2713") : "";
console.log(` \u2022 ${chalk.cyan(name)}${marker}`);
if (profile.description) {
console.log(chalk.gray(` ${profile.description}`));
}
});
});
config.command("test-score <tool>").description("Test importance scoring for a tool").option("-f, --files <number>", "Number of files affected", parseInt).option("-p, --permanent", "Is change permanent").option("-r, --references <number>", "Reference count", parseInt).action(async (tool, options) => {
const manager = new ConfigManager();
const score = manager.calculateScore(tool, {
filesAffected: options.files,
isPermanent: options.permanent,
referenceCount: options.references
});
const config2 = manager.getConfig();
const baseScore = config2.scoring.tool_scores[tool] || 0.5;
console.log(chalk.blue("\n\u{1F4CA} Score Calculation:"));
console.log(` Tool: ${chalk.cyan(tool)}`);
console.log(` Base Score: ${chalk.yellow(baseScore.toFixed(3))}`);
if (options.files !== void 0) {
console.log(` Files Affected: ${options.files}`);
}
if (options.permanent) {
console.log(` Permanent: ${chalk.green("Yes")}`);
}
if (options.references !== void 0) {
console.log(` References: ${options.references}`);
}
console.log(chalk.blue("\n Weights:"));
console.log(` Base: ${config2.scoring.weights.base}`);
console.log(` Impact: ${config2.scoring.weights.impact}`);
console.log(` Persistence: ${config2.scoring.weights.persistence}`);
console.log(` Reference: ${config2.scoring.weights.reference}`);
console.log(chalk.green(`
Final Score: ${score.toFixed(3)}`));
let level = "Low";
let color = chalk.gray;
if (score >= 0.8) {
level = "Critical";
color = chalk.red;
} else if (score >= 0.6) {
level = "High";
color = chalk.yellow;
} else if (score >= 0.4) {
level = "Medium";
color = chalk.cyan;
}
console.log(` Importance: ${color(level)}`);
});
config.command("create-profile <name>").description("Create a custom configuration profile").option("-d, --description <text>", "Profile description").option("-b, --base-weight <number>", "Base weight (0-1)", parseFloat).option("-i, --impact-weight <number>", "Impact weight (0-1)", parseFloat).option(
"-p, --persistence-weight <number>",
"Persistence weight (0-1)",
parseFloat
).option(
"-r, --reference-weight <number>",
"Reference weight (0-1)",
parseFloat
).option("--copy-from <profile>", "Copy settings from existing profile").action(async (name, options) => {
const manager = new ConfigManager();
const config2 = manager.getConfig();
if (config2.profiles && config2.profiles[name]) {
console.log(
chalk.yellow(
`\u26A0 Profile '${name}' already exists. Use 'edit-profile' to modify it.`
)
);
process.exit(1);
}
let newProfile;
if (options.copyFrom) {
const sourceProfile = config2.profiles?.[options.copyFrom];
if (!sourceProfile) {
console.log(
chalk.red(`\u274C Source profile '${options.copyFrom}' not found`)
);
process.exit(1);
}
newProfile = {
...sourceProfile,
name,
description: options.description
};
} else {
const weights = {
base: options.baseWeight ?? DEFAULT_WEIGHTS.base,
impact: options.impactWeight ?? DEFAULT_WEIGHTS.impact,
persistence: options.persistenceWeight ?? DEFAULT_WEIGHTS.persistence,
reference: options.referenceWeight ?? DEFAULT_WEIGHTS.reference
};
const sum = weights.base + weights.impact + weights.persistence + weights.reference;
if (Math.abs(sum - 1) > 1e-3) {
console.log(
chalk.red(`\u274C Weights must sum to 1.0 (current: ${sum.toFixed(3)})`)
);
console.log(chalk.yellow("\nNormalizing weights to sum to 1.0..."));
const factor = 1 / sum;
weights.base *= factor;
weights.impact *= factor;
weights.persistence *= factor;
weights.reference *= factor;
}
newProfile = {
name,
description: options.description || `Custom profile created ${(/* @__PURE__ */ new Date()).toLocaleDateString()}`,
scoring: {
weights,
tool_scores: DEFAULT_TOOL_SCORES
}
};
}
if (!config2.profiles) {
config2.profiles = {};
}
config2.profiles[name] = newProfile;
manager.save();
console.log(chalk.green(`\u2705 Created profile: ${name}`));
console.log(chalk.blue("\nProfile Configuration:"));
console.log(yaml.dump(newProfile, { indent: 2 }));
console.log(
chalk.cyan(`
Activate with: stackmemory config set-profile ${name}`)
);
});
config.command("edit-profile <name>").description("Edit an existing profile").option(
"-s, --set-tool <tool:score>",
"Set tool score (e.g., search:0.95)",
(value, previous) => {
const result = previous || {};
const [tool, score] = value.split(":");
result[tool] = parseFloat(score);
return result;
},
{}
).option(
"-w, --set-weight <type:value>",
"Set weight (e.g., base:0.4)",
(value, previous) => {
const result = previous || {};
const [type, weight] = value.split(":");
result[type] = parseFloat(weight);
return result;
},
{}
).action(async (name, options) => {
const manager = new ConfigManager();
const config2 = manager.getConfig();
if (!config2.profiles?.[name]) {
console.log(chalk.red(`\u274C Profile '${name}' not found`));
process.exit(1);
}
const profile = config2.profiles[name];
if (Object.keys(options.setTool).length > 0) {
if (!profile.scoring) {
profile.scoring = {};
}
if (!profile.scoring.tool_scores) {
profile.scoring.tool_scores = {};
}
Object.assign(profile.scoring.tool_scores, options.setTool);
console.log(chalk.green("\u2713 Updated tool scores"));
}
if (Object.keys(options.setWeight).length > 0) {
if (!profile.scoring) {
profile.scoring = {};
}
if (!profile.scoring.weights) {
profile.scoring.weights = { ...DEFAULT_WEIGHTS };
}
Object.assign(profile.scoring.weights, options.setWeight);
const weights = profile.scoring.weights;
const sum = (weights.base || 0) + (weights.impact || 0) + (weights.persistence || 0) + (weights.reference || 0);
if (Math.abs(sum - 1) > 1e-3) {
console.log(
chalk.yellow(`\u26A0 Weights sum to ${sum.toFixed(3)}, normalizing...`)
);
const factor = 1 / sum;
if (weights.base) weights.base *= factor;
if (weights.impact) weights.impact *= factor;
if (weights.persistence) weights.persistence *= factor;
if (weights.reference) weights.reference *= factor;
}
console.log(chalk.green("\u2713 Updated weights"));
}
manager.save();
console.log(chalk.green(`
\u2705 Profile '${name}' updated`));
console.log(chalk.blue("\nUpdated Configuration:"));
console.log(yaml.dump(profile, { indent: 2 }));
});
config.command("profile-report [profile]").description("Show profile effectiveness report").action(async (profile) => {
console.log(chalk.blue("\n\u{1F4CA} Profile Effectiveness Report"));
if (profile) {
console.log(chalk.cyan(`
Profile: ${profile}`));
console.log("Note: Run tools with this profile to generate metrics");
} else {
console.log(
"\nNote: Tool scoring metrics will be available after running MCP tools"
);
}
console.log(chalk.gray("\nMetrics tracked:"));
console.log(" \u2022 Average score per tool");
console.log(" \u2022 High-importance operations");
console.log(" \u2022 Profile usage frequency");
console.log(" \u2022 Score trends over time");
});
const storageCmd = config.command("storage").description(
`Manage storage configuration
Storage Modes:
sqlite (default): Local storage only, fast, no external dependencies
hybrid: SQLite + ChromaDB for semantic search and cloud backup`
);
storageCmd.command("show").description("Show current storage configuration").action(async () => {
const storageConfig = loadStorageConfig();
console.log(chalk.blue("\nStorage Configuration:"));
console.log(` Mode: ${chalk.cyan(storageConfig.mode)}`);
console.log(` Description: ${chalk.gray(getStorageModeDescription())}`);
if (storageConfig.chromadb.enabled) {
console.log(chalk.blue("\nChromaDB Settings:"));
console.log(` Enabled: ${chalk.green("Yes")}`);
console.log(
` API URL: ${chalk.gray(storageConfig.chromadb.apiUrl || "https://api.trychroma.com")}`
);
console.log(
` Tenant: ${chalk.gray(storageConfig.chromadb.tenant || "default_tenant")}`
);
console.log(
` Database: ${chalk.gray(storageConfig.chromadb.database || "default_database")}`
);
console.log(
` API Key: ${chalk.gray(storageConfig.chromadb.apiKey ? "[configured]" : "[not set]")}`
);
} else {
console.log(chalk.blue("\nChromaDB Settings:"));
console.log(` Enabled: ${chalk.yellow("No")}`);
console.log(
chalk.gray(
" Enable with: stackmemory config storage enable-chromadb"
)
);
}
});
storageCmd.command("enable-chromadb").description("Enable ChromaDB for semantic search and cloud backup").option("--api-key <key>", "ChromaDB API key").option("--api-url <url>", "ChromaDB API URL", "https://api.trychroma.com").action(async (options) => {
let apiKey = options.apiKey;
if (!apiKey && process.stdin.isTTY) {
const answers = await inquirer.prompt([
{
type: "password",
name: "apiKey",
message: "Enter your ChromaDB API key:",
validate: (input) => {
if (!input || input.trim().length === 0) {
return "API key is required for ChromaDB";
}
return true;
}
}
]);
apiKey = answers.apiKey;
}
if (!apiKey) {
console.log(chalk.red("[ERROR] ChromaDB API key is required."));
console.log(
chalk.gray("Provide via --api-key flag or run interactively.")
);
process.exit(1);
}
enableChromaDB({
apiKey,
apiUrl: options.apiUrl
});
console.log(chalk.green("[OK] ChromaDB enabled successfully."));
console.log(chalk.gray(`Storage mode: ${getStorageModeDescription()}`));
});
storageCmd.command("disable-chromadb").description("Disable ChromaDB and use SQLite-only storage").action(async () => {
disableChromaDB();
console.log(chalk.green("[OK] ChromaDB disabled."));
console.log(chalk.gray(`Storage mode: ${getStorageModeDescription()}`));
});
return config;
}
export {
createConfigCommand
};
//# sourceMappingURL=config.js.map