UNPKG

md-linear-sync

Version:

Sync Linear tickets to local markdown files with status-based folder organization

471 lines 15.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LinearClient = exports.LinearSyncClient = exports.LinearDiscoveryClient = void 0; const { LinearClient } = require('@linear/sdk'); exports.LinearClient = LinearClient; const RetryManager_1 = require("../utils/RetryManager"); class LinearDiscoveryClient { constructor(apiKey) { this.client = new LinearClient({ apiKey }); } async getTeams() { try { const response = await this.client.teams(); return response.nodes.map((team) => ({ id: team.id, name: team.name, key: team.key })); } catch (error) { throw new Error(`Failed to fetch teams: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getProjects(teamId) { try { // Get the team first, then get its projects directly const team = await this.client.team(teamId); const projectsConnection = await team.projects(); return projectsConnection.nodes.map((project) => ({ id: project.id, name: project.name, description: project.description || undefined })); } catch (error) { throw new Error(`Failed to fetch projects: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getWorkflowStates(teamId) { try { const response = await this.client.workflowStates({ filter: { team: { id: { eq: teamId } } } }); return response.nodes.map((state) => ({ id: state.id, name: state.name, type: state.type, position: state.position })).sort((a, b) => a.position - b.position); } catch (error) { throw new Error(`Failed to fetch workflow states: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getTeamLabels(teamId) { try { // Use direct GraphQL query through team object as the issueLabels filter doesn't work correctly const query = ` query GetTeamLabels($teamId: String!) { team(id: $teamId) { labels { nodes { id name color description } } } } `; const rawResponse = await this.client.client.rawRequest(query, { teamId }); const labels = rawResponse.data?.team?.labels?.nodes || []; return labels.map((label) => ({ id: label.id, name: label.name, color: label.color, description: label.description })); } catch (error) { throw new Error(`Failed to fetch team labels: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async validateApiKey() { try { const viewer = await this.client.viewer; return { valid: true, user: { name: viewer.name, email: viewer.email } }; } catch (error) { return { valid: false }; } } } exports.LinearDiscoveryClient = LinearDiscoveryClient; class LinearSyncClient { constructor(apiKey) { this.client = new LinearClient({ apiKey }); } async getIssue(issueId) { return RetryManager_1.RetryManager.withRetry(async () => { const query = ` query GetIssue($issueId: String!) { issue(id: $issueId) { id identifier title description url priority createdAt updatedAt dueDate branchName number labels { nodes { id name } } assignee { id name email } creator { id name email } parent { id identifier } state { id name type } comments(first: 50) { nodes { id body createdAt user { id name email } } } } } `; const variables = { issueId }; const rawResponse = await this.client.client.rawRequest(query, variables); const issue = rawResponse.data?.issue; if (!issue) { throw new Error(`Issue ${issueId} not found`); } return issue; }, {}, `fetch issue ${issueId}`); } async getIssues(teamId, projectId, limit = 100) { try { const filter = { team: { id: { eq: teamId } } }; if (projectId) { filter.project = { id: { eq: projectId } }; } // Use rawRequest to get headers and fetch issues with comments in one call const query = ` query IssuesWithCommentsQuery($filter: IssueFilter, $first: Int, $includeArchived: Boolean) { issues(filter: $filter, first: $first, includeArchived: $includeArchived) { nodes { id identifier title description url priority createdAt updatedAt dueDate branchName number labels { nodes { id name } } assignee { id name email } creator { id name email } parent { id identifier } state { id name type } comments(first: 50) { nodes { id body createdAt user { id name email } } } } } } `; const variables = { filter, first: limit, includeArchived: false }; const rawResponse = await this.client.client.rawRequest(query, variables); // Extract rate limit info from headers let apiUsage = undefined; if (rawResponse.headers) { const headers = rawResponse.headers; apiUsage = { requestsLimit: this.parseNumber(headers.get?.('x-ratelimit-requests-limit')), requestsRemaining: this.parseNumber(headers.get?.('x-ratelimit-requests-remaining')), requestsResetAt: this.parseNumber(headers.get?.('x-ratelimit-requests-reset')), note: "Rate limit info extracted from response headers" }; } return { issues: rawResponse.data?.issues?.nodes || [], apiUsage }; } catch (error) { // If it's a rate limit error, extract the rate limit info if (error instanceof Error && 'requestsRemaining' in error) { const rateLimitError = error; throw new Error(`Rate limit exceeded: ${rateLimitError.requestsRemaining}/${rateLimitError.requestsLimit} requests remaining`); } throw new Error(`Failed to fetch issues: ${error instanceof Error ? error.message : 'Unknown error'}`); } } parseNumber(value) { if (value === undefined || value === null || value === "") { return undefined; } return Number(value) ?? undefined; } async createIssue(teamId, title, description, stateId, projectId, labelIds, parentId, priority) { try { const issueInput = { teamId, title, description: description || '' }; if (stateId) issueInput.stateId = stateId; if (projectId) issueInput.projectId = projectId; if (labelIds && labelIds.length > 0) issueInput.labelIds = labelIds; if (parentId) issueInput.parentId = parentId; if (priority !== undefined) issueInput.priority = priority; const response = await this.client.createIssue(issueInput); return response.issue; } catch (error) { throw new Error(`Failed to create issue: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async findIssueByIdentifier(identifier) { try { const query = ` query FindIssueByIdentifier($identifier: String!) { issue(id: $identifier) { id identifier title } } `; const rawResponse = await this.client.client.rawRequest(query, { identifier }); return rawResponse.data?.issue || null; } catch (error) { // Issue not found is not an error - return null return null; } } async updateIssue(issueId, updates) { return RetryManager_1.RetryManager.withRetry(async () => { const response = await this.client.updateIssue(issueId, updates); return response.issue; }, {}, `update issue ${issueId}`); } async getComments(issueId) { try { const response = await this.client.comments({ filter: { issue: { id: { eq: issueId } } } }); return response.nodes; } catch (error) { throw new Error(`Failed to fetch comments for issue ${issueId}: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async createComment(issueId, body) { try { const response = await this.client.createComment({ issueId, body }); return response.comment; } catch (error) { throw new Error(`Failed to create comment: ${error instanceof Error ? error.message : 'Unknown error'}`); } } // Webhook management methods async getWebhooks() { try { const query = ` query GetWebhooks { webhooks { nodes { id url enabled secret team { id name } } } } `; const rawResponse = await this.client.client.rawRequest(query); return rawResponse.data?.webhooks?.nodes || []; } catch (error) { throw new Error(`Failed to fetch webhooks: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async createWebhook(input) { try { const mutation = ` mutation WebhookCreate($input: WebhookCreateInput!) { webhookCreate(input: $input) { success webhook { id url enabled } } } `; const variables = { input: { url: input.url, teamId: input.teamId, secret: input.secret || this.generateWebhookSecret(), resourceTypes: ["Issue", "Comment"] } }; const rawResponse = await this.client.client.rawRequest(mutation, variables); const result = rawResponse.data?.webhookCreate; if (!result?.success) { throw new Error('Failed to create webhook'); } return result.webhook.id; } catch (error) { throw new Error(`Failed to create webhook: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async updateWebhook(id, input) { try { const mutation = ` mutation WebhookUpdate($id: String!, $input: WebhookUpdateInput!) { webhookUpdate(id: $id, input: $input) { success webhook { id url enabled } } } `; const variables = { id, input }; const rawResponse = await this.client.client.rawRequest(mutation, variables); const result = rawResponse.data?.webhookUpdate; if (!result?.success) { throw new Error('Failed to update webhook'); } } catch (error) { throw new Error(`Failed to update webhook: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async deleteWebhook(id) { try { const mutation = ` mutation WebhookDelete($id: String!) { webhookDelete(id: $id) { success } } `; const variables = { id }; const rawResponse = await this.client.client.rawRequest(mutation, variables); const result = rawResponse.data?.webhookDelete; if (!result?.success) { throw new Error('Failed to delete webhook'); } } catch (error) { throw new Error(`Failed to delete webhook: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async upsertWebhook(input) { try { // Check if webhook exists for this team const existingWebhooks = await this.getWebhooks(); const ourWebhook = existingWebhooks.find(w => w.team?.id === input.teamId && w.url.includes('ngrok')); if (ourWebhook) { // Update existing await this.updateWebhook(ourWebhook.id, { url: input.url }); return ourWebhook.id; } else { // Create new return await this.createWebhook(input); } } catch (error) { throw new Error(`Failed to upsert webhook: ${error instanceof Error ? error.message : 'Unknown error'}`); } } generateWebhookSecret() { return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); } } exports.LinearSyncClient = LinearSyncClient; //# sourceMappingURL=index.js.map