@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.
227 lines (226 loc) • 6.06 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 { spawn, execSync } from "child_process";
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
import { join, dirname } from "path";
import {
DEFAULT_SERVER_CONFIG
} from "./types.js";
import { createPredictionClient } from "./prediction-client.js";
import { logger } from "../../core/monitoring/logger.js";
const HOME = process.env["HOME"] || "/tmp";
const PID_FILE = join(HOME, ".stackmemory", "sweep", "server.pid");
const LOG_FILE = join(HOME, ".stackmemory", "sweep", "server.log");
class SweepServerManager {
config;
process = null;
constructor(config = {}) {
this.config = { ...DEFAULT_SERVER_CONFIG, ...config };
if (!this.config.modelPath) {
this.config.modelPath = join(
HOME,
".stackmemory",
"models",
"sweep",
"sweep-next-edit-1.5b.q8_0.v2.gguf"
);
}
}
/**
* Find llama-server executable
*/
findLlamaServer() {
const candidates = [
"llama-server",
"llama.cpp/llama-server",
"/usr/local/bin/llama-server",
"/opt/homebrew/bin/llama-server",
join(HOME, ".local", "bin", "llama-server")
];
for (const cmd of candidates) {
try {
execSync(`which ${cmd}`, { stdio: "ignore" });
return cmd;
} catch {
if (existsSync(cmd)) {
return cmd;
}
}
}
return null;
}
/**
* Start the llama-server
*/
async startServer() {
const status = await this.getStatus();
if (status.running) {
return status;
}
if (!existsSync(this.config.modelPath)) {
throw new Error(
`Model not found: ${this.config.modelPath}
Download with: huggingface-cli download sweepai/sweep-next-edit-1.5B sweep-next-edit-1.5b.q8_0.v2.gguf --local-dir ~/.stackmemory/models/sweep`
);
}
const llamaServer = this.findLlamaServer();
if (!llamaServer) {
throw new Error(
"llama-server not found. Install with:\n brew install llama.cpp\nor build from source: https://github.com/ggerganov/llama.cpp"
);
}
const logDir = dirname(LOG_FILE);
if (!existsSync(logDir)) {
mkdirSync(logDir, { recursive: true });
}
const args = [
"-m",
this.config.modelPath,
"--port",
String(this.config.port),
"--host",
this.config.host,
"-c",
String(this.config.contextSize)
];
if (this.config.threads) {
args.push("-t", String(this.config.threads));
}
if (this.config.gpuLayers && this.config.gpuLayers > 0) {
args.push("-ngl", String(this.config.gpuLayers));
}
logger.info("Starting Sweep server", { llamaServer, args });
this.process = spawn(llamaServer, args, {
detached: true,
stdio: ["ignore", "pipe", "pipe"]
});
if (this.process.pid) {
const pidDir = dirname(PID_FILE);
if (!existsSync(pidDir)) {
mkdirSync(pidDir, { recursive: true });
}
writeFileSync(
PID_FILE,
JSON.stringify({
pid: this.process.pid,
port: this.config.port,
host: this.config.host,
modelPath: this.config.modelPath,
startedAt: Date.now()
})
);
}
this.process.unref();
const ready = await this.waitForReady(1e4);
if (!ready) {
await this.stopServer();
throw new Error("Server failed to start within timeout");
}
return this.getStatus();
}
/**
* Wait for server to be ready
*/
async waitForReady(timeoutMs) {
const client = createPredictionClient({
port: this.config.port,
host: this.config.host
});
const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
if (await client.checkHealth()) {
return true;
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
return false;
}
/**
* Stop the server
*/
async stopServer() {
const status = await this.getStatus();
if (!status.running || !status.pid) {
return;
}
try {
process.kill(status.pid, "SIGTERM");
await new Promise((resolve) => {
const checkInterval = setInterval(() => {
try {
process.kill(status.pid, 0);
} catch {
clearInterval(checkInterval);
resolve();
}
}, 100);
setTimeout(() => {
clearInterval(checkInterval);
try {
process.kill(status.pid, "SIGKILL");
} catch {
}
resolve();
}, 5e3);
});
} catch (error) {
logger.warn("Error stopping server", { error });
}
try {
if (existsSync(PID_FILE)) {
const { unlinkSync } = await import("fs");
unlinkSync(PID_FILE);
}
} catch {
}
}
/**
* Get server status
*/
async getStatus() {
if (!existsSync(PID_FILE)) {
return { running: false };
}
try {
const data = JSON.parse(readFileSync(PID_FILE, "utf-8"));
const { pid, port, host, modelPath, startedAt } = data;
try {
process.kill(pid, 0);
} catch {
return { running: false };
}
const client = createPredictionClient({ port, host });
const healthy = await client.checkHealth();
return {
running: healthy,
pid,
port,
host,
modelPath,
startedAt
};
} catch {
return { running: false };
}
}
/**
* Check server health
*/
async checkHealth() {
const client = createPredictionClient({
port: this.config.port,
host: this.config.host
});
return client.checkHealth();
}
}
function createServerManager(config) {
return new SweepServerManager(config);
}
export {
SweepServerManager,
createServerManager
};
//# sourceMappingURL=sweep-server-manager.js.map