UNPKG

@stackmemoryai/stackmemory

Version:

Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a

364 lines (363 loc) 14.1 kB
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"; 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"); }); return config; } export { createConfigCommand };