UNPKG

@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.

770 lines (765 loc) 29.4 kB
#!/usr/bin/env node 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 chalk from "chalk"; import ora from "ora"; import { initializeUnifiedOrchestrator } from "../../skills/unified-rlm-orchestrator.js"; import { DualStackManager } from "../../core/context/dual-stack-manager.js"; import { FrameHandoffManager } from "../../core/context/frame-handoff-manager.js"; import { FrameManager } from "../../core/context/index.js"; import { ContextRetriever } from "../../core/retrieval/context-retriever.js"; import { SQLiteAdapter } from "../../core/database/sqlite-adapter.js"; import { LinearTaskManager } from "../../features/tasks/linear-task-manager.js"; import { ConfigManager } from "../../core/config/config-manager.js"; import * as path from "path"; import * as os from "os"; import { SystemError, DatabaseError, ErrorCode } from "../../core/errors/index.js"; import { readFileSync } from "fs"; import { fileURLToPath } from "url"; let _version; function getVersion() { if (_version) return _version; try { const __dirname = path.dirname(fileURLToPath(import.meta.url)); let dir = __dirname; for (let i = 0; i < 6; i++) { const candidate = path.join(dir, "package.json"); try { _version = JSON.parse(readFileSync(candidate, "utf-8")).version; return _version; } catch { dir = path.dirname(dir); } } } catch { } _version = "0.0.0"; return _version; } function getEnv(key, defaultValue) { const value = process.env[key]; if (value === void 0) { if (defaultValue !== void 0) return defaultValue; throw new SystemError( `Environment variable ${key} is required`, ErrorCode.CONFIGURATION_ERROR, { variable: key } ); } return value; } function getOptionalEnv(key) { return process.env[key]; } async function initializeSkillContext() { const config = ConfigManager.getInstance(); const projectId = config.get("project.id") || "default-project"; const userId = config.get("user.id") || process.env["USER"] || "default"; const dbPath = path.join( os.homedir(), ".stackmemory", "data", projectId, "stackmemory.db" ); const database = new SQLiteAdapter(projectId, { dbPath }); await database.connect(); const rawDatabase = database.getRawDatabase(); if (!rawDatabase) { throw new DatabaseError( "Failed to get raw database connection", ErrorCode.DB_CONNECTION_FAILED, { projectId, operation: "initializeSkillContext" } ); } if (typeof rawDatabase.exec !== "function") { throw new DatabaseError( `Invalid database instance: missing exec() method. Got: ${typeof rawDatabase.exec}`, ErrorCode.DB_CONNECTION_FAILED, { projectId, operation: "initializeSkillContext" } ); } try { rawDatabase.exec("SELECT 1"); } catch (err) { throw new DatabaseError( `Database connection test failed: ${err.message}`, ErrorCode.DB_CONNECTION_FAILED, { projectId, operation: "initializeSkillContext" }, err ); } const dualStackManager = new DualStackManager(database, projectId, userId); const handoffManager = new FrameHandoffManager(dualStackManager); const contextRetriever = new ContextRetriever(database); const frameManager = new FrameManager(rawDatabase, projectId); const taskStore = new LinearTaskManager(); const context = { projectId, userId, dualStackManager, handoffManager, contextRetriever, database, frameManager }; const unifiedOrchestrator = initializeUnifiedOrchestrator( frameManager, dualStackManager, contextRetriever, taskStore, context ); return { context, unifiedOrchestrator }; } function createSkillsCommand() { const skillsCmd = new Command("skills").description( "Execute Claude skills for enhanced workflow" ); skillsCmd.command("handoff <targetUser> <message>").description("Streamline frame handoffs between team members").option( "-p, --priority <level>", "Set priority (low, medium, high, critical)", "medium" ).option("-f, --frames <frames...>", "Specific frames to handoff").option("--no-auto-detect", "Disable auto-detection of frames").action(async (targetUser, message, options) => { const spinner = ora("Initiating handoff...").start(); try { const { context, unifiedOrchestrator } = await initializeSkillContext(); const result = await unifiedOrchestrator.executeSkill( "handoff", [targetUser, message], { priority: options.priority, frames: options.frames, autoDetect: options.autoDetect !== false } ); spinner.stop(); if (result.success) { console.log(chalk.green("\u2713"), result.message); if (result.data) { console.log(chalk.cyan("\nHandoff Details:")); console.log(` ID: ${result.data.handoffId}`); console.log(` Frames: ${result.data.frameCount}`); console.log(` Priority: ${result.data.priority}`); if (result.data.actionItems?.length > 0) { console.log(chalk.yellow("\n Action Items:")); result.data.actionItems.forEach((item) => { console.log(` \u2022 ${item}`); }); } } } else { console.log(chalk.red("\u2717"), result.message); } await context.database.disconnect(); } catch (error) { spinner.stop(); console.error(chalk.red("Error:"), error.message); process.exit(1); } }); const checkpointCmd = skillsCmd.command("checkpoint").description("Create and manage recovery points"); checkpointCmd.command("create <description>").description("Create a new checkpoint").option("--files <files...>", "Include specific files in checkpoint").option("--auto-detect-risky", "Auto-detect risky operations").action(async (description, options) => { const spinner = ora("Creating checkpoint...").start(); try { const { context, unifiedOrchestrator } = await initializeSkillContext(); const result = await unifiedOrchestrator.executeSkill( "checkpoint", ["create", description], { includeFiles: options.files, autoDetectRisky: options.autoDetectRisky } ); spinner.stop(); if (result.success) { console.log(chalk.green("\u2713"), result.message); if (result.data) { console.log(chalk.cyan("\nCheckpoint Info:")); console.log(` ID: ${result.data.checkpointId}`); console.log(` Time: ${result.data.timestamp}`); console.log(` Frames: ${result.data.frameCount}`); } } else { console.log(chalk.red("\u2717"), result.message); } await context.database.disconnect(); } catch (error) { spinner.stop(); console.error(chalk.red("Error:"), error.message); process.exit(1); } }); checkpointCmd.command("restore <checkpointId>").description("Restore from a checkpoint").action(async (checkpointId) => { const spinner = ora("Restoring checkpoint...").start(); try { const { context, unifiedOrchestrator } = await initializeSkillContext(); const result = await unifiedOrchestrator.executeSkill("checkpoint", [ "restore", checkpointId ]); spinner.stop(); if (result.success) { console.log(chalk.green("\u2713"), result.message); if (result.data) { console.log(chalk.cyan("\nRestored:")); console.log(` Frames: ${result.data.frameCount}`); console.log(` Files: ${result.data.filesRestored}`); } } else { console.log(chalk.red("\u2717"), result.message); } await context.database.disconnect(); } catch (error) { spinner.stop(); console.error(chalk.red("Error:"), error.message); process.exit(1); } }); checkpointCmd.command("list").description("List available checkpoints").option("-l, --limit <number>", "Limit number of results", "10").option("-s, --since <date>", "Show checkpoints since date").action(async (options) => { const spinner = ora("Loading checkpoints...").start(); try { const { context, unifiedOrchestrator } = await initializeSkillContext(); const result = await unifiedOrchestrator.executeSkill( "checkpoint", ["list"], { limit: parseInt(options.limit), since: options.since ? new Date(options.since) : void 0 } ); spinner.stop(); if (result.success) { console.log(chalk.cyan("Available Checkpoints:\n")); if (result.data && result.data.length > 0) { result.data.forEach((cp) => { const riskIndicator = cp.risky ? chalk.yellow(" [RISKY]") : ""; console.log(`${chalk.bold(cp.id)}${riskIndicator}`); console.log(` ${cp.description}`); console.log( chalk.gray(` ${cp.timestamp} (${cp.frameCount} frames) `) ); }); } else { console.log(chalk.gray("No checkpoints found")); } } else { console.log(chalk.red("\u2717"), result.message); } await context.database.disconnect(); } catch (error) { spinner.stop(); console.error(chalk.red("Error:"), error.message); process.exit(1); } }); checkpointCmd.command("diff <checkpoint1> <checkpoint2>").description("Show differences between two checkpoints").action(async (checkpoint1, checkpoint2) => { const spinner = ora("Comparing checkpoints...").start(); try { const { context, unifiedOrchestrator } = await initializeSkillContext(); const result = await unifiedOrchestrator.executeSkill("checkpoint", [ "diff", checkpoint1, checkpoint2 ]); spinner.stop(); if (result.success) { console.log(chalk.cyan("Checkpoint Diff:\n")); if (result.data) { console.log(` Time difference: ${result.data.timeDiff}`); console.log(` Frame difference: ${result.data.framesDiff}`); console.log(` New frames: ${result.data.newFrames}`); console.log(` Removed frames: ${result.data.removedFrames}`); console.log(` Modified frames: ${result.data.modifiedFrames}`); } } else { console.log(chalk.red("\u2717"), result.message); } await context.database.disconnect(); } catch (error) { spinner.stop(); console.error(chalk.red("Error:"), error.message); process.exit(1); } }); skillsCmd.command("spike").description( "Run multi-agent spike (planner: Claude, implementer: Codex/Claude, critic: Claude)" ).option("-t, --task <desc>", "Task description", "Spike harness").option( "--planner-model <name>", "Claude model for planning", "claude-sonnet-4-20250514" ).option( "--reviewer-model <name>", "Claude model for review", "claude-sonnet-4-20250514" ).option("--implementer <name>", "codex|claude", "codex").option("--max-iters <n>", "Retry loop iterations", "2").option( "--execute", "Execute implementer (codex-sm) instead of dry-run", false ).option("--audit-dir <path>", "Persist spike results to directory").option("--record-frame", "Record as real frame with anchors", false).option( "--record", "Record plan & critique into StackMemory context", false ).option("--json", "Emit single JSON result (UI-friendly)", false).option("--quiet", "Minimal output (default)", true).option("--verbose", "Verbose sectioned output", false).action(async (options) => { const spinner = ora("Planning with Claude...").start(); try { const { runSpike } = await import("../../orchestrators/multimodal/harness.js"); const result = await runSpike( { task: options.task, repoPath: process.cwd() }, { plannerModel: options.plannerModel, reviewerModel: options.reviewerModel, implementer: options.implementer, maxIters: parseInt(options.maxIters), dryRun: !options.execute, auditDir: options.auditDir, recordFrame: Boolean(options.recordFrame), record: Boolean(options.record) } ); spinner.stop(); if (options.json) { console.log(JSON.stringify(result)); } else if (options.verbose) { console.log(chalk.gray(`StackMemory v${getVersion()}`)); console.log(chalk.cyan("\n=== Plan ===")); console.log(JSON.stringify(result.plan, null, 2)); console.log(chalk.cyan("\n=== Iterations ===")); (result.iterations || []).forEach((it, idx) => { console.log(chalk.gray(` -- Attempt ${idx + 1} --`)); console.log(`Command: ${it.command}`); console.log(`OK: ${it.ok}`); console.log("Critique:", JSON.stringify(it.critique)); }); console.log(chalk.cyan("\n=== Final ===")); console.log(JSON.stringify(result.implementation, null, 2)); console.log(chalk.cyan("\n=== Critique ===")); console.log(JSON.stringify(result.critique, null, 2)); } else if (!options.quiet) { console.log( `Plan steps: ${result.plan.steps.length}, Approved: ${result.critique.approved}` ); } if (!result.implementation.success) process.exitCode = 1; } catch (error) { spinner.stop(); console.error(chalk.red("Spike failed:"), error?.message || error); process.exit(1); } }); skillsCmd.command("plan <task>").description("Generate an implementation plan (no code execution)").option( "--planner-model <name>", "Claude model for planning", "claude-sonnet-4-20250514" ).option("--json", "Emit JSON (default)", true).option("--pretty", "Pretty-print JSON", false).option( "--compact", "Compact output (summary + step titles + criteria)", false ).action(async (task, options) => { const spinner = ora("Planning with Claude...").start(); try { const { runPlanOnly } = await import("../../orchestrators/multimodal/harness.js"); const plan = await runPlanOnly( { task, repoPath: process.cwd() }, { plannerModel: options.plannerModel } ); spinner.stop(); const compacted = options.compact ? { summary: plan?.summary, steps: Array.isArray(plan?.steps) ? plan.steps.map((s) => ({ id: s.id, title: s.title, acceptanceCriteria: s.acceptanceCriteria })) : [], risks: plan?.risks } : plan; const payload = JSON.stringify(compacted, null, options.pretty ? 2 : 0); console.log(payload); } catch (error) { spinner.stop(); console.error(chalk.red("Plan failed:"), error?.message || error); process.exit(1); } }); skillsCmd.command("dig <query>").description("Deep historical context retrieval").option( "-d, --depth <depth>", "Search depth (e.g., 30days, 6months, all)", "30days" ).option("--patterns", "Extract patterns from results").option("--decisions", "Extract key decisions").option("--timeline", "Generate activity timeline").action(async (query, options) => { const spinner = ora("Digging through context...").start(); try { const { context, unifiedOrchestrator } = await initializeSkillContext(); const result = await unifiedOrchestrator.executeSkill("dig", [query], { depth: options.depth, patterns: options.patterns, decisions: options.decisions, timeline: options.timeline }); spinner.stop(); if (result.success) { console.log(chalk.green("\u2713"), result.message); if (result.data) { console.log( chalk.cyan( ` Searched ${result.data.timeRange.from} to ${result.data.timeRange.to}` ) ); if (result.data.summary) { console.log("\n" + result.data.summary); } else { if (result.data.topResults?.length > 0) { console.log(chalk.cyan("\nTop Results:")); result.data.topResults.forEach((r) => { console.log( ` ${chalk.yellow(`[${r.score.toFixed(2)}]`)} ${r.summary}` ); }); } if (result.data.patterns?.length > 0) { console.log(chalk.cyan("\nDetected Patterns:")); result.data.patterns.forEach((p) => { console.log(` ${p.name}: ${p.count} occurrences`); }); } if (result.data.decisions?.length > 0) { console.log(chalk.cyan("\nKey Decisions:")); result.data.decisions.slice(0, 5).forEach((d) => { console.log( ` ${chalk.gray(new Date(d.timestamp).toLocaleDateString())}: ${d.decision}` ); }); } if (result.data.timeline?.length > 0) { console.log(chalk.cyan("\nActivity Timeline:")); result.data.timeline.slice(0, 5).forEach((t) => { console.log(` ${t.date}: ${t.itemCount} activities`); }); } } } } else { console.log(chalk.red("\u2717"), result.message); } await context.database.disconnect(); } catch (error) { spinner.stop(); console.error(chalk.red("Error:"), error.message); process.exit(1); } }); skillsCmd.command("rlm <task>").description("Execute complex tasks with recursive agent orchestration").option("--max-parallel <number>", "Maximum concurrent subagents", "5").option("--max-recursion <number>", "Maximum recursion depth", "4").option( "--max-tokens-per-agent <number>", "Token budget per subagent", "30000" ).option("--review-stages <number>", "Number of review iterations", "3").option( "--quality-threshold <number>", "Target quality score (0-1)", "0.85" ).option( "--test-mode <mode>", "Test generation mode (unit/integration/e2e/all)", "all" ).option("--verbose", "Show all recursive operations", false).option( "--share-context-realtime", "Share discoveries between agents", true ).option("--retry-failed-agents", "Retry on failure", true).option("--timeout-per-agent <number>", "Timeout in seconds", "300").action(async (task, options) => { const spinner = ora("Initializing RLM orchestrator...").start(); try { const { context, unifiedOrchestrator } = await initializeSkillContext(); spinner.text = "Decomposing task..."; const result = await unifiedOrchestrator.executeSkill("rlm", [task], { maxParallel: parseInt(options.maxParallel), maxRecursionDepth: parseInt(options.maxRecursion), maxTokensPerAgent: parseInt(options.maxTokensPerAgent), reviewStages: parseInt(options.reviewStages), qualityThreshold: parseFloat(options.qualityThreshold), testGenerationMode: options.testMode, verboseLogging: options.verbose, shareContextRealtime: options.shareContextRealtime, retryFailedAgents: options.retryFailedAgents, timeoutPerAgent: parseInt(options.timeoutPerAgent) * 1e3 }); spinner.stop(); if (result.success) { console.log(chalk.green("\u2713"), "RLM execution completed"); if (result.data) { console.log(chalk.cyan("\nExecution Summary:")); console.log(` Total tokens: ${result.data.totalTokens}`); console.log( ` Estimated cost: $${result.data.totalCost.toFixed(2)}` ); console.log(` Duration: ${result.data.duration}ms`); console.log(` Tests generated: ${result.data.testsGenerated}`); console.log(` Issues found: ${result.data.issuesFound}`); console.log(` Issues fixed: ${result.data.issuesFixed}`); if (result.data.improvements?.length > 0) { console.log(chalk.cyan("\nImprovements:")); result.data.improvements.forEach((imp) => { console.log(` \u2022 ${imp}`); }); } } } else { console.log(chalk.red("\u2717"), result.message); } await context.database.disconnect(); } catch (error) { spinner.stop(); console.error(chalk.red("Error:"), error.message); process.exit(1); } }); const specCmd = skillsCmd.command("spec").description("Generate iterative spec documents"); specCmd.command("generate <type> <title>").description( "Generate a spec document (one-pager, dev-spec, prompt-plan, agents)" ).action(async (type, title) => { const spinner = ora(`Generating ${type}...`).start(); try { const { context, unifiedOrchestrator } = await initializeSkillContext(); const result = await unifiedOrchestrator.executeSkill( "spec", ["generate", type, title], {} ); spinner.stop(); if (result.success) { console.log(chalk.green("\u2713"), result.message); } else { console.log(chalk.red("\u2717"), result.message); } await context.database.disconnect(); } catch (error) { spinner.stop(); console.error(chalk.red("Error:"), error.message); process.exit(1); } }); specCmd.command("list").description("List existing spec documents").action(async () => { try { const { context, unifiedOrchestrator } = await initializeSkillContext(); const result = await unifiedOrchestrator.executeSkill( "spec", ["list"], {} ); if (result.success) { console.log(chalk.green("\u2713"), result.message); if (result.data) { console.log(JSON.stringify(result.data, null, 2)); } } else { console.log(chalk.red("\u2717"), result.message); } await context.database.disconnect(); } catch (error) { console.error(chalk.red("Error:"), error.message); process.exit(1); } }); specCmd.command("validate <path>").description("Validate spec document completeness").action(async (filePath) => { try { const { context, unifiedOrchestrator } = await initializeSkillContext(); const result = await unifiedOrchestrator.executeSkill( "spec", ["validate", filePath], {} ); if (result.success) { console.log(chalk.green("\u2713"), result.message); } else { console.log(chalk.red("\u2717"), result.message); } await context.database.disconnect(); } catch (error) { console.error(chalk.red("Error:"), error.message); process.exit(1); } }); const linearRunCmd = skillsCmd.command("linear-run").description("Execute Linear tasks via RLM orchestrator"); linearRunCmd.command("next").description("Execute the next highest-priority Linear task").option("--priority <level>", "Filter by priority").option("--tag <tag>", "Filter by tag").option("--dry-run", "Preview without executing").action(async (options) => { const spinner = ora("Fetching next task...").start(); try { const { context, unifiedOrchestrator } = await initializeSkillContext(); const result = await unifiedOrchestrator.executeSkill( "linear-run", ["next"], { priority: options.priority, tag: options.tag, dryRun: options.dryRun } ); spinner.stop(); if (result.success) { console.log(chalk.green("\u2713"), result.message); if (result.data) { console.log(JSON.stringify(result.data, null, 2)); } } else { console.log(chalk.red("\u2717"), result.message); } await context.database.disconnect(); } catch (error) { spinner.stop(); console.error(chalk.red("Error:"), error.message); process.exit(1); } }); linearRunCmd.command("all").description("Execute all active Linear tasks iteratively").option("--max-concurrent <n>", "Max concurrent tasks", "1").option("--dry-run", "Preview without executing").action(async (options) => { const spinner = ora("Running all tasks...").start(); try { const { context, unifiedOrchestrator } = await initializeSkillContext(); const result = await unifiedOrchestrator.executeSkill( "linear-run", ["all"], { maxConcurrent: parseInt(options.maxConcurrent), dryRun: options.dryRun } ); spinner.stop(); if (result.success) { console.log(chalk.green("\u2713"), result.message); if (result.data) { console.log(JSON.stringify(result.data, null, 2)); } } else { console.log(chalk.red("\u2717"), result.message); } await context.database.disconnect(); } catch (error) { spinner.stop(); console.error(chalk.red("Error:"), error.message); process.exit(1); } }); linearRunCmd.command("task <taskId>").description("Execute a specific Linear task by ID").action(async (taskId) => { const spinner = ora(`Executing task ${taskId}...`).start(); try { const { context, unifiedOrchestrator } = await initializeSkillContext(); const result = await unifiedOrchestrator.executeSkill( "linear-run", ["task", taskId], {} ); spinner.stop(); if (result.success) { console.log(chalk.green("\u2713"), result.message); } else { console.log(chalk.red("\u2717"), result.message); } await context.database.disconnect(); } catch (error) { spinner.stop(); console.error(chalk.red("Error:"), error.message); process.exit(1); } }); linearRunCmd.command("preview [taskId]").description("Show execution plan without running").action(async (taskId) => { try { const { context, unifiedOrchestrator } = await initializeSkillContext(); const result = await unifiedOrchestrator.executeSkill( "linear-run", ["preview", taskId || ""], {} ); if (result.success) { console.log(chalk.green("\u2713"), result.message); if (result.data) { console.log(JSON.stringify(result.data, null, 2)); } } else { console.log(chalk.red("\u2717"), result.message); } await context.database.disconnect(); } catch (error) { console.error(chalk.red("Error:"), error.message); process.exit(1); } }); skillsCmd.command("help [skill]").description("Show help for a specific skill").action(async (skill) => { if (skill) { switch (skill) { case "lint": console.log(` lint (RLM-Orchestrated) Primary Agent: linting Secondary Agents: improve Comprehensive linting of code: Check syntax, types, formatting, security, performance, and dead code. Provide fixes. This skill is executed through RLM orchestration for: - Automatic task decomposition - Parallel agent execution - Multi-stage quality review - Comprehensive result aggregation Usage: stackmemory skills lint # Lint current directory stackmemory skills lint src/ # Lint specific directory stackmemory skills lint src/file.ts # Lint specific file Options: --fix Automatically fix issues where possible --format Focus on formatting issues --security Focus on security vulnerabilities --performance Focus on performance issues --verbose Show detailed output `); break; default: console.log( `Unknown skill: ${skill}. Use "stackmemory skills help" to see all available skills.` ); } } else { console.log( chalk.cyan("Available Claude Skills (RLM-Orchestrated):\n") ); console.log( " handoff - Streamline frame handoffs between team members" ); console.log(" checkpoint - Create and manage recovery points"); console.log(" dig - Deep historical context retrieval"); console.log( " lint - Comprehensive code linting and quality checks" ); console.log(" test - Generate comprehensive test suites"); console.log(" review - Multi-stage code review and improvements"); console.log(" refactor - Refactor code for better architecture"); console.log(" publish - Prepare and execute releases"); console.log(" rlm - Direct recursive agent orchestration"); console.log( " spec - Generate iterative spec docs (one-pager, dev-spec, prompt-plan, agents)" ); console.log( " linear-run - Execute Linear tasks via RLM orchestrator\n" ); console.log( chalk.yellow( "\nAll skills now use RLM orchestration for intelligent task decomposition" ) ); console.log( 'Use "stackmemory skills help <skill>" for detailed help on each skill' ); } }); return skillsCmd; } export { createSkillsCommand }; //# sourceMappingURL=skills.js.map