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.

187 lines (186 loc) 5.9 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 "../integrations/linear/client.js"; import { LinearAuthManager } from "../integrations/linear/auth.js"; const TEST_KEYWORDS = [ "test", "spec", "unit test", "integration test", "e2e", "end-to-end", "jest", "vitest", "mocha" ]; const VALIDATION_KEYWORDS = [ "validate", "verify", "verification", "acceptance criteria", "ac:", "acceptance:", "given when then", "criteria:" ]; const QA_KEYWORDS = ["qa", "quality", "regression", "coverage", "assertion"]; const TEST_LABELS = [ "needs-tests", "test-required", "qa-review", "has-ac", "acceptance-criteria", "tdd", "testing" ]; function containsKeywords(text, keywords) { const lowerText = text.toLowerCase(); return keywords.some((kw) => lowerText.includes(kw.toLowerCase())); } function scoreTask(issue, preferTestTasks) { let score = 0; const description = issue.description || ""; const title = issue.title || ""; const fullText = `${title} ${description}`; if (containsKeywords(fullText, TEST_KEYWORDS)) { score += preferTestTasks ? 10 : 5; } if (containsKeywords(fullText, VALIDATION_KEYWORDS)) { score += preferTestTasks ? 8 : 4; } if (containsKeywords(fullText, QA_KEYWORDS)) { score += preferTestTasks ? 5 : 2; } const labelNames = issue.labels?.nodes?.map((l) => l.name.toLowerCase()) || []; const hasTestLabel = TEST_LABELS.some( (tl) => labelNames.some((ln) => ln.includes(tl)) ); if (hasTestLabel) { score += preferTestTasks ? 5 : 3; } if (issue.priority === 1) { score += 5; } else if (issue.priority === 2) { score += 3; } else if (issue.priority === 3) { score += 1; } if (description.includes("## Acceptance") || description.includes("### AC") || description.includes("- [ ]")) { score += 2; } if (issue.estimate) { score += 1; } return score; } function getLinearClient() { const apiKey = process.env["LINEAR_API_KEY"]; if (apiKey && apiKey.startsWith("lin_api_")) { return new LinearClient({ apiKey }); } try { const authManager = new LinearAuthManager(); const tokens = authManager.loadTokens(); if (tokens?.accessToken) { return new LinearClient({ accessToken: tokens.accessToken }); } } catch { } return null; } async function pickNextLinearTask(options = {}) { const client = getLinearClient(); if (!client) { return null; } const { teamId, preferTestTasks = true, limit = 20 } = options; try { const [backlogIssues, unstartedIssues] = await Promise.all([ client.getIssues({ teamId, stateType: "backlog", limit }), client.getIssues({ teamId, stateType: "unstarted", limit }) ]); const allIssues = [...backlogIssues, ...unstartedIssues]; const unassignedIssues = allIssues.filter((issue) => !issue.assignee); if (unassignedIssues.length === 0) { if (allIssues.length === 0) { return null; } } const issuesToScore = unassignedIssues.length > 0 ? unassignedIssues : allIssues; const scoredIssues = issuesToScore.map((issue) => ({ issue, score: scoreTask(issue, preferTestTasks) })); scoredIssues.sort((a, b) => b.score - a.score); const best = scoredIssues[0]; if (!best) { return null; } const description = best.issue.description || ""; const hasTestRequirements = containsKeywords(description, TEST_KEYWORDS) || containsKeywords(description, VALIDATION_KEYWORDS); return { id: best.issue.id, identifier: best.issue.identifier, title: best.issue.title, priority: best.issue.priority, hasTestRequirements, estimatedPoints: best.issue.estimate, url: best.issue.url, score: best.score }; } catch (error) { const isAuthError = error instanceof Error && (error.message.includes("401") || error.message.includes("403")); if (!isAuthError) { console.error("[linear-task-picker] Error fetching tasks:", error); } return null; } } async function getTopTaskSuggestions(options = {}, count = 3) { const client = getLinearClient(); if (!client) { return []; } const { teamId, preferTestTasks = true, limit = 30 } = options; try { const [backlogIssues, unstartedIssues] = await Promise.all([ client.getIssues({ teamId, stateType: "backlog", limit }), client.getIssues({ teamId, stateType: "unstarted", limit }) ]); const allIssues = [...backlogIssues, ...unstartedIssues]; const unassignedIssues = allIssues.filter((issue) => !issue.assignee); const issuesToScore = unassignedIssues.length > 0 ? unassignedIssues : allIssues; const scoredIssues = issuesToScore.map((issue) => ({ issue, score: scoreTask(issue, preferTestTasks) })); scoredIssues.sort((a, b) => b.score - a.score); return scoredIssues.slice(0, count).map(({ issue, score }) => { const description = issue.description || ""; const hasTestRequirements = containsKeywords(description, TEST_KEYWORDS) || containsKeywords(description, VALIDATION_KEYWORDS); return { id: issue.id, identifier: issue.identifier, title: issue.title, priority: issue.priority, hasTestRequirements, estimatedPoints: issue.estimate, url: issue.url, score }; }); } catch (error) { const isAuthError = error instanceof Error && (error.message.includes("401") || error.message.includes("403")); if (!isAuthError) { console.error("[linear-task-picker] Error fetching tasks:", error); } return []; } } export { getTopTaskSuggestions, pickNextLinearTask }; //# sourceMappingURL=linear-task-picker.js.map