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.

227 lines (226 loc) 6.06 kB
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