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.

173 lines (148 loc) • 4.12 kB
#!/usr/bin/env node /** * Show Linear task summary with all statuses */ import { readFileSync } from 'fs'; import { join } from 'path'; interface LinearIssue { identifier: string; title: string; state: { name: string; type: string; }; createdAt: string; updatedAt: string; } async function showLinearSummary() { // Load Linear tokens const tokensPath = join(process.cwd(), '.stackmemory', 'linear-tokens.json'); let accessToken: string; try { const tokensData = readFileSync(tokensPath, 'utf8'); const tokens = JSON.parse(tokensData); accessToken = tokens.accessToken; } catch { console.error( 'āŒ Failed to load Linear tokens. Please run: stackmemory linear setup' ); process.exit(1); } // GraphQL helper const linearApiUrl = 'https://api.linear.app/graphql'; async function graphqlRequest( query: string, variables: Record<string, unknown> = {} ) { const response = await fetch(linearApiUrl, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ query, variables }), }); if (!response.ok) { throw new Error( `Linear API error: ${response.status} ${response.statusText}` ); } const result = (await response.json()) as { errors?: unknown[]; data: unknown; }; if (result.errors) { throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`); } return result.data; } // Get issues grouped by state const issuesQuery = ` query { issues(first: 250) { nodes { identifier title state { name type } createdAt updatedAt } } } `; const data = (await graphqlRequest(issuesQuery)) as { issues: { nodes: LinearIssue[] }; }; const issues = data.issues.nodes; // Group by state type const grouped = new Map<string, LinearIssue[]>(); for (const issue of issues) { const stateType = issue.state.type; if (!grouped.has(stateType)) { grouped.set(stateType, []); } const stateIssues = grouped.get(stateType); if (stateIssues) { stateIssues.push(issue); } } // Display summary console.log('\nšŸ“Š Linear Task Summary\n'); console.log('='.repeat(60)); const stateOrder = [ 'backlog', 'unstarted', 'started', 'completed', 'canceled', ]; for (const state of stateOrder) { const stateIssues = grouped.get(state) || []; if (stateIssues.length === 0) continue; const emoji = { backlog: 'šŸ“‹', unstarted: 'ā³', started: 'šŸ”„', completed: 'āœ…', canceled: 'āŒ', }[state] || '⚪'; console.log( `\n${emoji} ${state.toUpperCase()} (${stateIssues.length} tasks)` ); console.log('-'.repeat(40)); // Show recent items (last 5) const recent = stateIssues .sort( (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() ) .slice(0, state === 'canceled' ? 20 : 5); for (const issue of recent) { const updatedDate = new Date(issue.updatedAt).toLocaleDateString(); console.log( ` ${issue.identifier.padEnd(10)} ${issue.title.slice(0, 50).padEnd(50)} ${updatedDate}` ); } if (stateIssues.length > recent.length) { console.log(` ... and ${stateIssues.length - recent.length} more`); } } console.log('\n' + '='.repeat(60)); console.log('\nšŸ“ˆ Total Issues: ' + issues.length); const activeCount = (grouped.get('started')?.length ?? 0) + (grouped.get('unstarted')?.length ?? 0) + (grouped.get('backlog')?.length ?? 0); console.log(' Active: ' + activeCount); console.log(' Completed: ' + (grouped.get('completed')?.length ?? 0)); console.log(' Canceled: ' + (grouped.get('canceled')?.length ?? 0)); } // Run showLinearSummary().catch((error) => { console.error('āŒ Error:', error); process.exit(1); });