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