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.

232 lines (231 loc) 7.2 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { LinearClient } from "./client.js"; import { ContextService } from "../../services/context-service.js"; import { ConfigService } from "../../services/config-service.js"; import { logger } from "../../core/monitoring/logger.js"; import { IntegrationError, ErrorCode } from "../../core/errors/index.js"; class LinearSyncService { linearClient; contextService; configService; // Using singleton logger from monitoring constructor() { this.configService = new ConfigService(); this.contextService = new ContextService(); const apiKey = process.env["LINEAR_API_KEY"]; if (!apiKey) { throw new IntegrationError( "LINEAR_API_KEY environment variable not set", ErrorCode.LINEAR_AUTH_FAILED ); } this.linearClient = new LinearClient({ apiKey }); } async syncAllIssues() { const result = { created: 0, updated: 0, deleted: 0, conflicts: 0, errors: [] }; try { const config = await this.configService.getConfig(); const teamId = config.integrations?.linear?.teamId; if (!teamId) { throw new IntegrationError( "Linear team ID not configured", ErrorCode.LINEAR_SYNC_FAILED ); } const issues = await this.linearClient.getIssues({ teamId }); for (const issue of issues) { try { const synced = await this.syncIssueToLocal(issue); if (synced === "created") result.created++; else if (synced === "updated") result.updated++; } catch (error) { const message = error instanceof Error ? error.message : String(error); result.errors.push(`Failed to sync ${issue.identifier}: ${message}`); } } logger.info( `Sync complete: ${result.created} created, ${result.updated} updated` ); } catch (error) { logger.error("Sync failed:", error); const message = error instanceof Error ? error.message : String(error); result.errors.push(message); } return result; } async syncIssueToLocal(issue) { try { const task = this.convertIssueToTask(issue); const existingTask = await this.contextService.getTaskByExternalId( issue.id ); if (existingTask) { if (this.hasChanges(existingTask, task)) { await this.contextService.updateTask(existingTask.id, task); logger.debug(`Updated task: ${issue.identifier}`); return "updated"; } return "skipped"; } else { await this.contextService.createTask(task); logger.debug(`Created task: ${issue.identifier}`); return "created"; } } catch (error) { logger.error(`Failed to sync issue ${issue.identifier}:`, error); throw error; } } async syncLocalToLinear(taskId) { try { const task = await this.contextService.getTask(taskId); if (!task) { throw new IntegrationError( `Task ${taskId} not found`, ErrorCode.LINEAR_SYNC_FAILED, { taskId } ); } if (task.externalId) { const updateData = this.convertTaskToUpdateData(task); const updated = await this.linearClient.updateIssue( task.externalId, updateData ); logger.debug(`Updated Linear issue: ${updated.identifier}`); return updated; } else { const config = await this.configService.getConfig(); const teamId = config.integrations?.linear?.teamId; if (!teamId) { throw new IntegrationError( "Linear team ID not configured", ErrorCode.LINEAR_SYNC_FAILED ); } const createData = { title: task.title, description: task.description, teamId, priority: this.mapTaskPriorityToLinearPriority(task.priority) }; const created = await this.linearClient.createIssue(createData); await this.contextService.updateTask(taskId, { externalId: created.id }); logger.debug(`Created Linear issue: ${created.identifier}`); return created; } } catch (error) { logger.error(`Failed to sync task ${taskId} to Linear:`, error); throw error; } } async removeLocalIssue(identifier) { try { const tasks = await this.contextService.getAllTasks(); const task = tasks.find((t) => t.externalIdentifier === identifier); if (task) { await this.contextService.deleteTask(task.id); logger.debug(`Removed local task: ${identifier}`); } } catch (error) { logger.error(`Failed to remove task ${identifier}:`, error); throw error; } } convertIssueToTask(issue) { return { title: issue.title, description: issue.description || "", status: this.mapLinearStateToTaskStatus(issue.state.type), priority: this.mapLinearPriorityToTaskPriority(issue.priority), externalId: issue.id, externalIdentifier: issue.identifier, externalUrl: issue.url, tags: issue.labels?.map((l) => l.name) || [], metadata: { linear: { stateId: issue.state.id, stateName: issue.state.name, assigneeId: issue.assignee?.id, assigneeName: issue.assignee?.name } }, updatedAt: new Date(issue.updatedAt) }; } convertTaskToUpdateData(task) { return { title: task.title, description: task.description, priority: this.mapTaskPriorityToLinearPriority(task.priority), stateId: task.metadata?.linear?.stateId }; } mapLinearStateToTaskStatus(state) { switch (state.toLowerCase()) { case "backlog": case "triage": return "todo"; case "unstarted": case "todo": return "todo"; case "started": case "in_progress": return "in_progress"; case "completed": case "done": return "done"; case "canceled": case "cancelled": return "cancelled"; default: return "todo"; } } mapTaskPriorityToLinearPriority(priority) { switch (priority) { case "urgent": return 1; case "high": return 2; case "medium": return 3; case "low": return 4; default: return 0; } } mapLinearPriorityToTaskPriority(priority) { switch (priority) { case 1: return "urgent"; case 2: return "high"; case 3: return "medium"; case 4: return "low"; default: return void 0; } } hasChanges(existing, updated) { return existing.title !== updated.title || existing.description !== updated.description || existing.status !== updated.status || existing.priority !== updated.priority || JSON.stringify(existing.tags) !== JSON.stringify(updated.tags); } } export { LinearSyncService }; //# sourceMappingURL=sync-service.js.map