Spaces:
Runtime error
Runtime error
Download integrations/linear/src/schedule.ts from BTankut/Context-Store-LLM: direct link, hf CLI and curl.
- Browser
- Download file 13.9 kB
-
https://huggingface.co/spaces/BTankut/Context-Store-LLM/resolve/main/integrations/linear/src/schedule.ts
- Command line
-
hf download hf://spaces/BTankut/Context-Store-LLM/integrations/linear/src/schedule.ts
-
curl -L -o schedule.ts https://huggingface.co/spaces/BTankut/Context-Store-LLM/resolve/main/integrations/linear/src/schedule.ts
13.9 kB
| /* eslint-disable @typescript-eslint/no-explicit-any */ | |
| import axios from 'axios'; | |
| interface LinearActivityCreateParams { | |
| text: string; | |
| sourceURL: string; | |
| } | |
| interface LinearSettings { | |
| lastIssuesSync?: string; | |
| lastCommentsSync?: string; | |
| lastUserActionsSync?: string; | |
| } | |
| /** | |
| * Creates an activity message based on Linear data | |
| */ | |
| function createActivityMessage(params: LinearActivityCreateParams) { | |
| return { | |
| type: 'activity', | |
| data: { | |
| text: params.text, | |
| sourceURL: params.sourceURL, | |
| }, | |
| }; | |
| } | |
| /** | |
| * Fetches user information from Linear | |
| */ | |
| async function fetchUserInfo(apiKey: string) { | |
| try { | |
| const query = ` | |
| query { | |
| viewer { | |
| id | |
| name | |
| } | |
| } | |
| `; | |
| const response = await axios.post( | |
| 'https://api.linear.app/graphql', | |
| { query }, | |
| { | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| Authorization: apiKey, | |
| }, | |
| }, | |
| ); | |
| return response.data.data.viewer; | |
| } catch (error) { | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Fetches recent issues relevant to the user (created, assigned, or subscribed) | |
| */ | |
| async function fetchRecentIssues(apiKey: string, lastSyncTime: string) { | |
| try { | |
| const query = ` | |
| query RecentIssues($lastSyncTime: DateTimeOrDuration) { | |
| issues( | |
| filter: { | |
| updatedAt: { gt: $lastSyncTime } | |
| }, | |
| first: 50, | |
| orderBy: updatedAt | |
| ) { | |
| nodes { | |
| id | |
| identifier | |
| title | |
| url | |
| createdAt | |
| updatedAt | |
| state { | |
| id | |
| name | |
| type | |
| } | |
| team { | |
| id | |
| name | |
| } | |
| assignee { | |
| id | |
| name | |
| displayName | |
| } | |
| creator { | |
| id | |
| name | |
| displayName | |
| } | |
| subscribers { | |
| nodes { | |
| id | |
| name | |
| displayName | |
| } | |
| } | |
| priority | |
| history { | |
| nodes { | |
| id | |
| createdAt | |
| updatedAt | |
| fromStateId | |
| toStateId | |
| fromAssigneeId | |
| toAssigneeId | |
| fromPriority | |
| toPriority | |
| } | |
| } | |
| } | |
| pageInfo { | |
| hasNextPage | |
| endCursor | |
| } | |
| } | |
| } | |
| `; | |
| const response = await axios.post( | |
| 'https://api.linear.app/graphql', | |
| { | |
| query, | |
| variables: { | |
| lastSyncTime, | |
| }, | |
| }, | |
| { | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| Authorization: apiKey, | |
| }, | |
| }, | |
| ); | |
| return response.data.data.issues; | |
| } catch (error) { | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Fetches recent comments on issues relevant to the user | |
| */ | |
| async function fetchRecentComments(apiKey: string, lastSyncTime: string) { | |
| try { | |
| const query = ` | |
| query RecentComments($lastSyncTime: DateTimeOrDuration) { | |
| comments( | |
| filter: { | |
| updatedAt: { gt: $lastSyncTime } | |
| }, | |
| first: 50, | |
| orderBy: updatedAt | |
| ) { | |
| nodes { | |
| id | |
| body | |
| createdAt | |
| updatedAt | |
| user { | |
| id | |
| name | |
| displayName | |
| } | |
| issue { | |
| id | |
| identifier | |
| title | |
| url | |
| creator { | |
| id | |
| name | |
| displayName | |
| } | |
| assignee { | |
| id | |
| name | |
| displayName | |
| } | |
| subscribers { | |
| nodes { | |
| id | |
| name | |
| displayName | |
| } | |
| } | |
| } | |
| } | |
| pageInfo { | |
| hasNextPage | |
| endCursor | |
| } | |
| } | |
| } | |
| `; | |
| const response = await axios.post( | |
| 'https://api.linear.app/graphql', | |
| { | |
| query, | |
| variables: { | |
| lastSyncTime, | |
| }, | |
| }, | |
| { | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| Authorization: apiKey, | |
| }, | |
| }, | |
| ); | |
| return response.data.data.comments; | |
| } catch (error) { | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Process issue activities and create appropriate activity records | |
| */ | |
| async function processIssueActivities(issues: any[], userId: string) { | |
| const activities = []; | |
| for (const issue of issues) { | |
| try { | |
| // Skip issues that don't involve the user | |
| const isAssignee = issue.assignee?.id === userId; | |
| const isCreatedByUser = issue.creator?.id === userId; | |
| // Check if user is subscribed to the issue | |
| const isSubscribed = | |
| issue.subscribers?.nodes?.some((subscriber: any) => subscriber.id === userId) || false; | |
| if (!isAssignee && !isCreatedByUser && !isSubscribed) { | |
| continue; | |
| } | |
| // Process history to determine what actually changed | |
| let activityCreated = false; | |
| // Process assignment changes first (highest priority) | |
| if (issue.history && issue.history.nodes) { | |
| for (const historyItem of issue.history.nodes) { | |
| if (historyItem.toAssigneeId && historyItem.fromAssigneeId !== historyItem.toAssigneeId) { | |
| if (historyItem.toAssigneeId === userId) { | |
| activities.push( | |
| createActivityMessage({ | |
| text: `${issue.identifier} (${issue.title}) Issue assigned to you`, | |
| sourceURL: issue.url, | |
| }), | |
| ); | |
| activityCreated = true; | |
| break; | |
| } else if (isCreatedByUser && historyItem.fromAssigneeId === userId) { | |
| activities.push( | |
| createActivityMessage({ | |
| text: `${issue.identifier} (${issue.title}) Issue unassigned from you`, | |
| sourceURL: issue.url, | |
| }), | |
| ); | |
| activityCreated = true; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| // If no assignment change, check for status changes | |
| if (!activityCreated && issue.history && issue.history.nodes) { | |
| for (const historyItem of issue.history.nodes) { | |
| if (historyItem.toStateId && historyItem.fromStateId !== historyItem.toStateId) { | |
| if (!isAssignee && !isCreatedByUser && !isSubscribed) { | |
| continue; | |
| } | |
| const stateType = issue.state?.type; | |
| let statusText = `moved to ${issue.state?.name || 'a new status'}`; | |
| if (stateType === 'completed') { | |
| statusText = 'completed'; | |
| } else if (stateType === 'canceled') { | |
| statusText = 'canceled'; | |
| } | |
| let title; | |
| if (isCreatedByUser || isAssignee) { | |
| title = `${issue.identifier} (${issue.title}) Issue ${statusText}`; | |
| } else { | |
| title = `${issue.identifier} (${issue.title}) Issue ${statusText}`; | |
| } | |
| activities.push( | |
| createActivityMessage({ | |
| text: title, | |
| sourceURL: issue.url, | |
| }), | |
| ); | |
| activityCreated = true; | |
| break; | |
| } | |
| } | |
| } | |
| // If no history changes, check if it's a new issue creation | |
| if (!activityCreated && isCreatedByUser) { | |
| // Only create activity if issue was created recently (within sync window) | |
| const createdAt = new Date(issue.createdAt); | |
| const updatedAt = new Date(issue.updatedAt); | |
| // If created and updated times are very close, it's likely a new issue | |
| if (Math.abs(createdAt.getTime() - updatedAt.getTime()) < 60000) { | |
| // within 1 minute | |
| activities.push( | |
| createActivityMessage({ | |
| text: `${issue.identifier} (${issue.title}) Issue created`, | |
| sourceURL: issue.url, | |
| }), | |
| ); | |
| activityCreated = true; | |
| } | |
| } | |
| } catch (error) { | |
| // Silently ignore errors to prevent stdout pollution | |
| } | |
| } | |
| return activities; | |
| } | |
| /** | |
| * Process comment activities and create appropriate activity records | |
| */ | |
| async function processCommentActivities(comments: any[], userId: string, userInfo: any) { | |
| const activities = []; | |
| for (const comment of comments) { | |
| try { | |
| const isCommenter = comment.user?.id === userId; | |
| const isIssueCreator = comment.issue?.creator?.id === userId; | |
| const isAssignee = comment.issue?.assignee?.id === userId; | |
| // Check if user is subscribed to the issue | |
| const isSubscribed = | |
| comment.issue?.subscribers?.nodes?.some((subscriber: any) => subscriber.id === userId) || | |
| false; | |
| // Check for mentions in the comment body | |
| const isMentioned = checkForUserMentions(comment.body, userInfo); | |
| // Skip if not relevant to user | |
| if (!isCommenter && !isIssueCreator && !isAssignee && !isSubscribed && !isMentioned) { | |
| continue; | |
| } | |
| let title; | |
| if (isCommenter) { | |
| // Comment created by the user | |
| title = `You commented on issue ${comment.issue.identifier}: ${truncateText(comment.body, 100)}`; | |
| } else if (isMentioned) { | |
| // User was mentioned in the comment | |
| title = `${comment.user?.name || 'Someone'} mentioned you in issue ${comment.issue.identifier}: ${truncateText(comment.body, 100)}`; | |
| } else if (isAssignee || isIssueCreator || isSubscribed) { | |
| // Comment on issue where user is assignee, creator, or subscriber | |
| let relation = 'an issue'; | |
| if (isAssignee) { | |
| relation = 'your assigned issue'; | |
| } else if (isIssueCreator) { | |
| relation = 'your issue'; | |
| } else if (isSubscribed) { | |
| relation = "an issue you're subscribed to"; | |
| } | |
| title = `${comment.user?.name || 'Someone'} commented on ${relation} ${comment.issue.identifier}: ${truncateText(comment.body, 100)}`; | |
| } | |
| if (title) { | |
| activities.push( | |
| createActivityMessage({ | |
| text: title, | |
| sourceURL: `${comment.issue.url}#comment-${comment.id}`, | |
| }), | |
| ); | |
| } | |
| } catch (error) { | |
| // Silently ignore errors to prevent stdout pollution | |
| } | |
| } | |
| return activities; | |
| } | |
| /** | |
| * Helper function to check for user mentions in text | |
| */ | |
| function checkForUserMentions(text: string, userInfo: any): boolean { | |
| if (!text || !userInfo) return false; | |
| const lowerText = text.toLowerCase(); | |
| // Check for @username, @display name, or @email mentions | |
| const mentionPatterns = [ | |
| userInfo.name && `@${userInfo.name.toLowerCase()}`, | |
| userInfo.displayName && `@${userInfo.displayName.toLowerCase()}`, | |
| userInfo.email && `@${userInfo.email.toLowerCase()}`, | |
| ].filter(Boolean); | |
| return mentionPatterns.some((pattern) => lowerText.includes(pattern)); | |
| } | |
| /** | |
| * Helper function to truncate text with ellipsis | |
| */ | |
| function truncateText(text: string, maxLength: number): string { | |
| if (!text) return ''; | |
| return text.length > maxLength ? text.substring(0, maxLength) + '...' : text; | |
| } | |
| /** | |
| * Helper function to get default sync time (24 hours ago) | |
| */ | |
| function getDefaultSyncTime(): string { | |
| const yesterday = new Date(); | |
| yesterday.setDate(yesterday.getDate() - 1); | |
| return yesterday.toISOString(); | |
| } | |
| /** | |
| * Main function to handle scheduled sync for Linear integration | |
| */ | |
| export async function handleSchedule(config: any, state: any) { | |
| try { | |
| const integrationConfiguration = config; | |
| // Check if we have a valid access token | |
| if (!integrationConfiguration?.apiKey) { | |
| return []; | |
| } | |
| // Get settings or initialize if not present | |
| let settings = (state || {}) as LinearSettings; | |
| // Default to 24 hours ago if no last sync times | |
| const lastIssuesSync = settings.lastIssuesSync || getDefaultSyncTime(); | |
| const lastCommentsSync = settings.lastCommentsSync || getDefaultSyncTime(); | |
| // Fetch user info to identify activities relevant to them | |
| let user; | |
| try { | |
| user = await fetchUserInfo(integrationConfiguration.apiKey); | |
| } catch (error) { | |
| return []; | |
| } | |
| if (!user || !user.id) { | |
| return []; | |
| } | |
| // Collect all messages | |
| const messages = []; | |
| // Process all issue activities (created, assigned, updated, etc.) | |
| try { | |
| const issues = await fetchRecentIssues(integrationConfiguration.apiKey, lastIssuesSync); | |
| if (issues && issues.nodes) { | |
| const issueActivities = await processIssueActivities(issues.nodes, user.id); | |
| messages.push(...issueActivities); | |
| } | |
| } catch (error) { | |
| // Silently ignore errors to prevent stdout pollution | |
| } | |
| // Process all comment activities | |
| try { | |
| const comments = await fetchRecentComments(integrationConfiguration.apiKey, lastCommentsSync); | |
| if (comments && comments.nodes) { | |
| const commentActivities = await processCommentActivities(comments.nodes, user.id, user); | |
| messages.push(...commentActivities); | |
| } | |
| } catch (error) { | |
| // Silently ignore errors to prevent stdout pollution | |
| } | |
| // Update last sync times | |
| const newSyncTime = new Date().toISOString(); | |
| // Add state message for saving settings | |
| messages.push({ | |
| type: 'state', | |
| data: { | |
| ...settings, | |
| lastIssuesSync: newSyncTime, | |
| lastCommentsSync: newSyncTime, | |
| }, | |
| }); | |
| return messages; | |
| } catch (error) { | |
| return []; | |
| } | |
| } | |
| /** | |
| * The main handler for the scheduled sync event | |
| */ | |
| export async function scheduleHandler(config: any, state: any) { | |
| return handleSchedule(config, state); | |
| } | |