@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.
259 lines (258 loc) • 7.77 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 { 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
};
//# sourceMappingURL=auto-sync.js.map