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

258 lines (257 loc) 7.74 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { logger } from "../../core/monitoring/logger.js"; import { LinearTaskManager } from "../../features/tasks/linear-task-manager.js"; import { LinearAuthManager } from "./auth.js"; import { LinearSyncEngine, DEFAULT_SYNC_CONFIG } from "./sync.js"; import { LinearConfigManager } from "./config.js"; import { IntegrationError, ErrorCode } from "../../core/errors/index.js"; import Database from "better-sqlite3"; import { join } from "path"; import { existsSync } from "fs"; class LinearAutoSyncService { config; projectRoot; configManager; syncEngine; intervalId; isRunning = false; lastSyncTime = 0; retryCount = 0; constructor(projectRoot, config) { this.projectRoot = projectRoot; this.configManager = new LinearConfigManager(projectRoot); const persistedConfig = this.configManager.loadConfig(); const baseConfig = persistedConfig ? this.configManager.toAutoSyncConfig(persistedConfig) : { ...DEFAULT_SYNC_CONFIG, enabled: true, interval: 5, retryAttempts: 3, retryDelay: 3e4, autoSync: true, direction: "bidirectional", conflictResolution: "newest_wins", quietHours: { start: 22, end: 7 } }; this.config = { ...baseConfig, ...config }; if (config && Object.keys(config).length > 0) { this.configManager.saveConfig(config); } } /** * Start the auto-sync service */ async start() { if (this.isRunning) { logger.warn("Linear auto-sync service is already running"); return; } try { const authManager = new LinearAuthManager(this.projectRoot); if (!authManager.isConfigured()) { throw new IntegrationError( 'Linear integration not configured. Run "stackmemory linear setup" first.', ErrorCode.LINEAR_AUTH_FAILED ); } const dbPath = join(this.projectRoot, ".stackmemory", "context.db"); if (!existsSync(dbPath)) { throw new IntegrationError( 'StackMemory not initialized. Run "stackmemory init" first.', ErrorCode.LINEAR_SYNC_FAILED ); } const db = new Database(dbPath); const taskStore = new LinearTaskManager(this.projectRoot, db); this.syncEngine = new LinearSyncEngine( taskStore, authManager, this.config ); const token = await authManager.getValidToken(); if (!token) { throw new IntegrationError( "Unable to get valid Linear token. Check authentication.", ErrorCode.LINEAR_AUTH_FAILED ); } this.isRunning = true; this.scheduleNextSync(); logger.info("Linear auto-sync service started", { interval: this.config.interval, direction: this.config.direction, conflictResolution: this.config.conflictResolution }); this.performSync(); } catch (error) { logger.error("Failed to start Linear auto-sync service:", error); throw error; } } /** * Stop the auto-sync service */ stop() { if (this.intervalId) { clearTimeout(this.intervalId); this.intervalId = void 0; } this.isRunning = false; logger.info("Linear auto-sync service stopped"); } /** * Get service status */ getStatus() { const nextSyncTime = this.intervalId ? this.lastSyncTime + this.config.interval * 60 * 1e3 : void 0; return { running: this.isRunning, lastSyncTime: this.lastSyncTime, nextSyncTime, retryCount: this.retryCount, config: this.config }; } /** * Update configuration */ updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; if (this.isRunning) { this.stop(); this.start(); } logger.info("Linear auto-sync config updated", newConfig); } /** * Force immediate sync */ async forceSync() { if (!this.syncEngine) { throw new IntegrationError( "Sync engine not initialized", ErrorCode.LINEAR_SYNC_FAILED ); } logger.info("Forcing immediate Linear sync"); await this.performSync(); } /** * Schedule next sync based on configuration */ scheduleNextSync() { if (!this.isRunning) return; const delay = this.config.interval * 60 * 1e3; this.intervalId = setTimeout(() => { if (this.isRunning) { this.performSync(); } }, delay); } /** * Perform synchronization with error handling and retries */ async performSync() { if (!this.syncEngine) { logger.error("Sync engine not available"); return; } if (this.isInQuietHours()) { logger.debug("Skipping sync during quiet hours"); this.scheduleNextSync(); return; } try { logger.debug("Starting Linear auto-sync"); const result = await this.syncEngine.sync(); if (result.success) { this.lastSyncTime = Date.now(); this.retryCount = 0; const hasChanges = result.synced.toLinear > 0 || result.synced.fromLinear > 0 || result.synced.updated > 0; if (hasChanges) { logger.info("Linear auto-sync completed with changes", { toLinear: result.synced.toLinear, fromLinear: result.synced.fromLinear, updated: result.synced.updated, conflicts: result.conflicts.length }); } else { logger.debug("Linear auto-sync completed - no changes"); } if (result.conflicts.length > 0) { logger.warn("Linear sync conflicts detected", { count: result.conflicts.length, conflicts: result.conflicts.map((c) => ({ taskId: c.taskId, reason: c.reason })) }); } } else { throw new IntegrationError( `Sync failed: ${result.errors.join(", ")}`, ErrorCode.LINEAR_SYNC_FAILED ); } } catch (error) { logger.error("Linear auto-sync failed:", error); this.retryCount++; if (this.retryCount <= this.config.retryAttempts) { logger.info( `Retrying Linear sync in ${this.config.retryDelay / 1e3}s (attempt ${this.retryCount}/${this.config.retryAttempts})` ); setTimeout(() => { if (this.isRunning) { this.performSync(); } }, this.config.retryDelay); return; } else { logger.error( `Linear auto-sync failed after ${this.config.retryAttempts} attempts, skipping until next interval` ); this.retryCount = 0; } } this.scheduleNextSync(); } /** * Check if current time is within quiet hours */ isInQuietHours() { if (!this.config.quietHours) return false; const now = /* @__PURE__ */ new Date(); const currentHour = now.getHours(); const { start, end } = this.config.quietHours; if (start < end) { return currentHour >= start || currentHour < end; } else { return currentHour >= start && currentHour < end; } } } let autoSyncService = null; function initializeAutoSync(projectRoot, config) { if (autoSyncService) { autoSyncService.stop(); } autoSyncService = new LinearAutoSyncService(projectRoot, config); return autoSyncService; } function getAutoSyncService() { return autoSyncService; } function stopAutoSync() { if (autoSyncService) { autoSyncService.stop(); autoSyncService = null; } } export { LinearAutoSyncService, getAutoSyncService, initializeAutoSync, stopAutoSync };