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.

250 lines (248 loc) 9.29 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 chalk from "chalk"; import ora from "ora"; import { existsSync } from "fs"; import { join } from "path"; import { createServerManager, createPredictionClient, launchWrapper, DEFAULT_SERVER_CONFIG } from "../../features/sweep/index.js"; const HOME = process.env["HOME"] || "/tmp"; function createSweepCommand() { const cmd = new Command("sweep").description("Manage Sweep Next-Edit prediction server").addHelpText( "after", ` Examples: stackmemory sweep start Start the Sweep server stackmemory sweep stop Stop the Sweep server stackmemory sweep status Check server status stackmemory sweep predict Run a prediction manually stackmemory sweep hook Install/check Claude Code hook The Sweep server uses the Sweep 1.5B model for next-edit predictions. Predictions are triggered after file edits via Claude Code hooks. ` ); cmd.command("start").description("Start the Sweep prediction server").option( "--port <number>", "Server port", String(DEFAULT_SERVER_CONFIG.port) ).option("--model <path>", "Path to GGUF model file").option( "--context <size>", "Context size", String(DEFAULT_SERVER_CONFIG.contextSize) ).option("--gpu-layers <n>", "Number of GPU layers (0 for CPU only)", "0").action(async (options) => { const spinner = ora("Starting Sweep server...").start(); try { const config = { port: parseInt(options.port, 10), contextSize: parseInt(options.context, 10), gpuLayers: parseInt(options.gpuLayers, 10) }; if (options.model) { config.modelPath = options.model; } const manager = createServerManager(config); const status = await manager.startServer(); spinner.succeed(chalk.green("Sweep server started")); console.log(chalk.gray(` PID: ${status.pid}`)); console.log(chalk.gray(` Port: ${status.port}`)); console.log(chalk.gray(` Model: ${status.modelPath}`)); } catch (error) { spinner.fail(chalk.red("Failed to start Sweep server")); console.error(chalk.red(error.message)); process.exit(1); } }); cmd.command("stop").description("Stop the Sweep prediction server").action(async () => { const spinner = ora("Stopping Sweep server...").start(); try { const manager = createServerManager(); await manager.stopServer(); spinner.succeed(chalk.green("Sweep server stopped")); } catch (error) { spinner.fail(chalk.red("Failed to stop Sweep server")); console.error(chalk.red(error.message)); process.exit(1); } }); cmd.command("status").description("Check Sweep server status").action(async () => { try { const manager = createServerManager(); const status = await manager.getStatus(); if (status.running) { console.log(chalk.green("Sweep server is running")); console.log(chalk.gray(` PID: ${status.pid}`)); console.log(chalk.gray(` Port: ${status.port}`)); console.log(chalk.gray(` Host: ${status.host}`)); if (status.startedAt) { const uptime = Math.floor((Date.now() - status.startedAt) / 1e3); console.log(chalk.gray(` Uptime: ${formatUptime(uptime)}`)); } if (status.modelPath) { console.log(chalk.gray(` Model: ${status.modelPath}`)); } } else { console.log(chalk.yellow("Sweep server is not running")); console.log(chalk.gray(" Start with: stackmemory sweep start")); } const defaultModelPath = join( HOME, ".stackmemory", "models", "sweep", "sweep-next-edit-1.5b.q8_0.v2.gguf" ); if (!existsSync(defaultModelPath)) { console.log(""); console.log(chalk.yellow("Model not found at default location")); console.log( chalk.gray( " Download with:\n huggingface-cli download sweepai/sweep-next-edit-1.5B \\\n sweep-next-edit-1.5b.q8_0.v2.gguf \\\n --local-dir ~/.stackmemory/models/sweep" ) ); } } catch (error) { console.error( chalk.red("Error checking status:"), error.message ); process.exit(1); } }); cmd.command("predict").description("Run a prediction manually (for testing)").argument("<file>", "File to predict edits for").option( "--port <number>", "Server port", String(DEFAULT_SERVER_CONFIG.port) ).action(async (file, options) => { const spinner = ora("Running prediction...").start(); try { const { readFileSync } = await import("fs"); const content = readFileSync(file, "utf-8"); const client = createPredictionClient({ port: parseInt(options.port, 10) }); const healthy = await client.checkHealth(); if (!healthy) { spinner.fail(chalk.red("Server not running")); console.log(chalk.gray("Start with: stackmemory sweep start")); process.exit(1); } const result = await client.predict({ file_path: file, current_content: content, recent_diffs: [] }); spinner.stop(); if (result.success && result.predicted_content) { console.log(chalk.green("Prediction complete")); console.log(chalk.gray(`Latency: ${result.latency_ms}ms`)); console.log(chalk.gray(`Tokens: ${result.tokens_generated}`)); console.log(""); console.log(chalk.cyan("Predicted content:")); console.log(result.predicted_content.slice(0, 500)); if (result.predicted_content.length > 500) { console.log(chalk.gray("... (truncated)")); } } else if (result.success) { console.log(chalk.yellow("No changes predicted")); } else { console.log(chalk.red("Prediction failed:"), result.message); } } catch (error) { spinner.fail(chalk.red("Prediction failed")); console.error(chalk.red(error.message)); process.exit(1); } }); cmd.command("hook").description("Check Claude Code hook status").action(async () => { const hookPath = join(HOME, ".claude", "hooks", "post-edit-sweep.js"); const templatePath = join( process.cwd(), "templates", "claude-hooks", "post-edit-sweep.js" ); console.log(chalk.cyan("Sweep Hook Status")); console.log(""); if (existsSync(hookPath)) { console.log(chalk.green("Hook installed:"), hookPath); } else { console.log(chalk.yellow("Hook not installed")); console.log(chalk.gray(` Copy from: ${templatePath}`)); console.log(chalk.gray(` To: ${hookPath}`)); } const settingsPath = join(HOME, ".claude", "settings.json"); if (existsSync(settingsPath)) { try { const { readFileSync } = await import("fs"); const settings = JSON.parse(readFileSync(settingsPath, "utf-8")); const hasHook = JSON.stringify(settings).includes("post-edit-sweep"); if (hasHook) { console.log(chalk.green("Hook registered in settings.json")); } else { console.log(chalk.yellow("Hook not registered in settings.json")); console.log( chalk.gray( ' Add to hooks.PostToolUse:\n { "matcher": "Edit", "hooks": [{ "type": "command", "command": "node ~/.claude/hooks/post-edit-sweep.js" }] }' ) ); } } catch { console.log(chalk.yellow("Could not read settings.json")); } } else { console.log(chalk.yellow("settings.json not found")); } const statePath = join(HOME, ".stackmemory", "sweep-state.json"); if (existsSync(statePath)) { try { const { readFileSync } = await import("fs"); const state = JSON.parse(readFileSync(statePath, "utf-8")); console.log( chalk.gray( ` Recent diffs tracked: ${state.recentDiffs?.length || 0}` ) ); if (state.lastPrediction) { const ago = Math.floor( (Date.now() - state.lastPrediction.timestamp) / 1e3 ); console.log( chalk.gray(` Last prediction: ${formatUptime(ago)} ago`) ); } } catch { } } }); cmd.command("wrap").description("Launch Claude Code with Sweep prediction status bar").option("--claude-bin <path>", "Path to claude binary").allowUnknownOption(true).action(async (options, command) => { try { const claudeArgs = command.args || []; await launchWrapper({ claudeBin: options.claudeBin, claudeArgs }); } catch (error) { console.error(chalk.red(error.message)); process.exit(1); } }); return cmd; } function formatUptime(seconds) { if (seconds < 60) return `${seconds}s`; if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`; const hours = Math.floor(seconds / 3600); const mins = Math.floor(seconds % 3600 / 60); return `${hours}h ${mins}m`; } export { createSweepCommand }; //# sourceMappingURL=sweep.js.map