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

327 lines (326 loc) 9.28 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 "../../linear/client.js"; import { logger } from "../../../core/monitoring/logger.js"; class LinearHandlers { constructor(deps) { this.deps = deps; } /** * Create an authenticated LinearClient from the auth manager token */ async getClient() { const token = await this.deps.linearAuthManager.getValidToken(); return new LinearClient({ apiKey: token, useBearer: true }); } /** * Sync tasks with Linear */ async handleLinearSync(args) { try { const { direction = "both", force = false } = args; try { await this.deps.linearAuthManager.getValidToken(); } catch { return { content: [ { type: "text", text: "Linear auth required. Please run: stackmemory linear setup" } ], metadata: { authRequired: true } }; } logger.info("Starting Linear sync", { direction, force }); const result = await this.deps.linearSync.sync(); const syncText = `Linear Sync Complete: - To Linear: ${result.synced.toLinear} tasks - From Linear: ${result.synced.fromLinear} tasks - Updated: ${result.synced.updated} tasks - Errors: ${result.errors.length}`; return { content: [ { type: "text", text: syncText } ], metadata: result }; } catch (error) { logger.error( "Linear sync failed", error instanceof Error ? error : new Error(String(error)) ); const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage?.includes("unauthorized") || errorMessage?.includes("auth")) { return { content: [ { type: "text", text: "Linear authentication failed. Please run: stackmemory linear setup" } ], metadata: { authError: true } }; } throw error; } } /** * Update Linear issue directly via GraphQL API */ async handleLinearUpdateTask(args) { try { const { linear_id, status, assignee_id, priority, labels } = args; if (!linear_id) { throw new Error("Linear ID is required"); } const client = await this.getClient(); const updateData = {}; if (status) updateData.stateId = status; if (assignee_id) updateData.assigneeId = assignee_id; if (priority !== void 0) updateData.priority = priority; if (labels) { updateData.labelIds = Array.isArray(labels) ? labels : [labels]; } const issue = await client.updateIssue(linear_id, updateData); return { content: [ { type: "text", text: `Updated ${issue.identifier}: ${issue.title} Status: ${issue.state.name} | Priority: ${issue.priority}` } ], metadata: { id: issue.id, identifier: issue.identifier, state: issue.state.name, url: issue.url } }; } catch (error) { logger.error( "Error updating Linear task", error instanceof Error ? error : new Error(String(error)) ); throw error; } } /** * Get issues from Linear via GraphQL API */ async handleLinearGetTasks(args) { try { const { team_id, assignee_id, state = "active", limit = 20 } = args; const client = await this.getClient(); const stateTypeMap = { active: "started", closed: "completed", all: void 0 }; const issues = await client.getIssues({ teamId: team_id, assigneeId: assignee_id, stateType: stateTypeMap[state], limit }); const issueLines = issues.map( (i) => `${i.identifier} [${i.state.name}] ${i.title}${i.assignee ? ` (@${i.assignee.name})` : ""}` ); const text = issues.length > 0 ? `Found ${issues.length} issues: ${issueLines.join("\n")}` : "No issues found matching filters."; return { content: [{ type: "text", text }], metadata: { count: issues.length, issues: issues.map((i) => ({ id: i.id, identifier: i.identifier, title: i.title, state: i.state.name, priority: i.priority, assignee: i.assignee?.name, url: i.url })) } }; } catch (error) { logger.error( "Error getting Linear tasks", error instanceof Error ? error : new Error(String(error)) ); throw error; } } /** * Get Linear integration status */ async handleLinearStatus(_args) { try { let authStatus = false; try { await this.deps.linearAuthManager.getValidToken(); authStatus = true; } catch { authStatus = false; } if (!authStatus) { return { content: [ { type: "text", text: "Linear: Not connected\nRun: stackmemory linear setup" } ], metadata: { connected: false, authRequired: true } }; } const statusText = "Linear Integration Status:\n\u2713 Connected (authenticated)\n\nUse `stackmemory linear sync` for full sync details."; return { content: [ { type: "text", text: statusText } ], metadata: { connected: true } }; } catch (error) { logger.error( "Error getting Linear status", error instanceof Error ? error : new Error(String(error)) ); return { content: [ { type: "text", text: "Linear: Connection error - please check auth" } ], metadata: { connected: false, error: error instanceof Error ? error.message : String(error) } }; } } /** * Create a comment on a Linear issue */ async handleLinearCreateComment(args) { try { const { issue_id, body } = args; if (!issue_id || !body) { throw new Error("issue_id and body are required"); } const client = await this.getClient(); const comment = await client.createComment(issue_id, body); return { content: [ { type: "text", text: `Comment created on ${issue_id} ID: ${comment.id} Preview: ${body.slice(0, 100)}${body.length > 100 ? "..." : ""}` } ], metadata: { id: comment.id, issueId: issue_id, createdAt: comment.createdAt } }; } catch (error) { logger.error( "Error creating Linear comment", error instanceof Error ? error : new Error(String(error)) ); throw error; } } /** * Update an existing comment on a Linear issue */ async handleLinearUpdateComment(args) { try { const { comment_id, body } = args; if (!comment_id || !body) { throw new Error("comment_id and body are required"); } const client = await this.getClient(); const comment = await client.updateComment(comment_id, body); return { content: [ { type: "text", text: `Comment ${comment_id} updated Preview: ${body.slice(0, 100)}${body.length > 100 ? "..." : ""}` } ], metadata: { id: comment.id, updatedAt: comment.updatedAt } }; } catch (error) { logger.error( "Error updating Linear comment", error instanceof Error ? error : new Error(String(error)) ); throw error; } } /** * List comments on a Linear issue */ async handleLinearListComments(args) { try { const { issue_id } = args; if (!issue_id) { throw new Error("issue_id is required"); } const client = await this.getClient(); const comments = await client.getComments(issue_id); const lines = comments.map( (c) => `${c.id.slice(0, 8)} | ${c.user?.name ?? "unknown"} | ${new Date(c.createdAt).toISOString().slice(0, 10)} | ${c.body.slice(0, 60).replace(/\n/g, " ")}${c.body.length > 60 ? "..." : ""}` ); const text = comments.length > 0 ? `${comments.length} comments on ${issue_id}: ${lines.join("\n")}` : `No comments on ${issue_id}`; return { content: [{ type: "text", text }], metadata: { count: comments.length, comments: comments.map((c) => ({ id: c.id, author: c.user?.name, createdAt: c.createdAt, bodyPreview: c.body.slice(0, 200) })) } }; } catch (error) { logger.error( "Error listing Linear comments", error instanceof Error ? error : new Error(String(error)) ); throw error; } } } export { LinearHandlers };