UNPKG

@stackmemoryai/stackmemory

Version:

Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a

222 lines (221 loc) 6.73 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { execFileSync } from "child_process"; import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs"; import { join } from "path"; import { homedir } from "os"; import { logger } from "../monitoring/logger.js"; import { ErrorCode, getErrorMessage, wrapError } from "../errors/index.js"; import { withTimeout, gracefulDegrade } from "../errors/recovery.js"; class UpdateChecker { static CACHE_FILE = join( homedir(), ".stackmemory", "update-check.json" ); static CHECK_INTERVAL = 24 * 60 * 60 * 1e3; // 24 hours static PACKAGE_NAME = "@stackmemoryai/stackmemory"; /** * Check for updates and display notification if needed */ static async checkForUpdates(currentVersion, silent = false) { try { const cache = this.loadCache(); const now = Date.now(); if (cache && now - cache.lastChecked < this.CHECK_INTERVAL) { if (!silent && cache.latestVersion && cache.latestVersion !== currentVersion) { this.displayUpdateNotification(currentVersion, cache.latestVersion); } return; } const latestVersion = await this.fetchLatestVersion(); this.saveCache({ lastChecked: now, latestVersion, currentVersion }); if (!silent && latestVersion && this.isNewerVersion(currentVersion, latestVersion)) { this.displayUpdateNotification(currentVersion, latestVersion); } } catch (error) { const wrappedError = wrapError( error, "Update check failed", ErrorCode.INTERNAL_ERROR, { currentVersion, silent } ); logger.debug("Update check failed:", { error: getErrorMessage(error), context: wrappedError.context }); } } /** * Fetch latest version from npm registry */ static async fetchLatestVersion() { try { const fetchVersion = async () => { const output = execFileSync( "npm", ["view", this.PACKAGE_NAME, "version"], { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"], timeout: 5e3 // 5 second timeout } ).trim(); return output; }; return await gracefulDegrade( () => withTimeout(fetchVersion, 5e3, "npm registry timeout"), "", { operation: "fetchLatestVersion", package: this.PACKAGE_NAME } ); } catch (error) { const wrappedError = wrapError( error, "Failed to fetch latest version from npm", ErrorCode.SERVICE_UNAVAILABLE, { package: this.PACKAGE_NAME } ); logger.debug("Failed to fetch latest version:", { error: getErrorMessage(error), context: wrappedError.context }); return ""; } } /** * Compare version strings */ static isNewerVersion(current, latest) { try { const currentParts = current.split(".").map(Number); const latestParts = latest.split(".").map(Number); if (currentParts.some(isNaN) || latestParts.some(isNaN)) { logger.debug("Invalid version format:", { current, latest }); return false; } for (let i = 0; i < 3; i++) { const latestPart = latestParts[i] ?? 0; const currentPart = currentParts[i] ?? 0; if (latestPart > currentPart) return true; if (latestPart < currentPart) return false; } return false; } catch (error) { logger.debug("Version comparison failed:", { error: getErrorMessage(error), current, latest }); return false; } } /** * Display update notification */ static displayUpdateNotification(current, latest) { console.log("\n" + "\u2500".repeat(60)); console.log("\u{1F4E6} StackMemory Update Available!"); console.log(` Current: v${current}`); console.log(` Latest: v${latest}`); console.log("\n Update with:"); console.log(" npm install -g @stackmemoryai/stackmemory@latest"); console.log("\u2500".repeat(60) + "\n"); } /** * Load update cache */ static loadCache() { try { if (!existsSync(this.CACHE_FILE)) { return null; } const data = readFileSync(this.CACHE_FILE, "utf-8"); const cache = JSON.parse(data); if (typeof cache.lastChecked !== "number" || typeof cache.latestVersion !== "string" || typeof cache.currentVersion !== "string") { logger.debug("Invalid cache format, ignoring"); return null; } return cache; } catch (error) { const wrappedError = wrapError( error, "Failed to load update cache", ErrorCode.INTERNAL_ERROR, { cacheFile: this.CACHE_FILE } ); logger.debug("Failed to load update cache:", { error: getErrorMessage(error), context: wrappedError.context }); return null; } } /** * Save update cache */ static saveCache(cache) { try { const dir = join(homedir(), ".stackmemory"); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true, mode: 493 }); } const tempFile = `${this.CACHE_FILE}.tmp`; writeFileSync(tempFile, JSON.stringify(cache, null, 2), { mode: 420 }); if (existsSync(this.CACHE_FILE)) { writeFileSync(this.CACHE_FILE, JSON.stringify(cache, null, 2)); } else { writeFileSync(this.CACHE_FILE, JSON.stringify(cache, null, 2)); } } catch (error) { const wrappedError = wrapError( error, "Failed to save update cache", ErrorCode.INTERNAL_ERROR, { cacheFile: this.CACHE_FILE, cache } ); logger.debug("Failed to save update cache:", { error: getErrorMessage(error), context: wrappedError.context }); } } /** * Force check for updates (ignores cache) */ static async forceCheck(currentVersion) { try { const latestVersion = await this.fetchLatestVersion(); this.saveCache({ lastChecked: Date.now(), latestVersion, currentVersion }); if (latestVersion) { if (this.isNewerVersion(currentVersion, latestVersion)) { this.displayUpdateNotification(currentVersion, latestVersion); } else { console.log(`\u2705 StackMemory is up to date (v${currentVersion})`); } } } catch (error) { console.error("\u274C Update check failed:", error.message); } } } export { UpdateChecker };