@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.
237 lines (203 loc) ⢠6.73 kB
JavaScript
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({
path: path.join(__dirname, '..', '.env'),
silent: true
});
async function queryLinear(query, variables = {}) {
const response = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': process.env.STACKMEMORY_LINEAR_API_KEY || process.env.LINEAR_API_KEY
},
body: JSON.stringify({ query, variables })
});
const data = await response.json();
if (data.errors) {
throw new Error(data.errors[0].message);
}
return data.data;
}
async function reviewTasks() {
console.log('š LINEAR TASK REVIEW\n' + '='.repeat(50));
try {
// Get viewer info
const viewerData = await queryLinear(`
query {
viewer {
name
email
}
}
`);
console.log(`š¤ Logged in as: ${viewerData.viewer.name || viewerData.viewer.email}\n`);
// Get all active issues
const issuesData = await queryLinear(`
query {
issues(
filter: {
state: { type: { in: ["started", "unstarted"] } }
}
orderBy: updatedAt
first: 100
) {
nodes {
identifier
title
priority
priorityLabel
createdAt
updatedAt
state {
name
type
}
assignee {
name
email
}
labels {
nodes {
name
color
}
}
estimate
url
}
}
}
`);
const issues = issuesData.issues.nodes;
// Group by state
const byState = {};
issues.forEach(issue => {
const stateName = issue.state.name;
if (!byState[stateName]) byState[stateName] = [];
byState[stateName].push(issue);
});
// Priority mapping
const priorityLabels = {
0: 'š“ No Priority',
1: 'š“ Urgent',
2: 'š High',
3: 'š” Medium',
4: 'šµ Low'
};
// Display by state
const stateOrder = ['In Progress', 'Todo', 'Triage', 'Backlog'];
stateOrder.forEach(state => {
if (byState[state] && byState[state].length > 0) {
console.log(`\nš ${state.toUpperCase()} (${byState[state].length} tasks)`);
console.log('-'.repeat(50));
byState[state]
.sort((a, b) => (a.priority || 5) - (b.priority || 5))
.slice(0, 10)
.forEach(issue => {
const priority = priorityLabels[issue.priority] || 'āŖ None';
const assignee = issue.assignee ? `š¤ ${issue.assignee.name}` : 'š¤ Unassigned';
const labels = issue.labels.nodes.map(l => l.name).join(', ');
const labelStr = labels ? ` [${labels}]` : '';
console.log(`\n ${issue.identifier}: ${issue.title}`);
console.log(` ${priority} | ${assignee}${labelStr}`);
console.log(` š ${issue.url}`);
});
if (byState[state].length > 10) {
console.log(`\n ... and ${byState[state].length - 10} more tasks`);
}
}
});
// Summary statistics
console.log('\n' + '='.repeat(50));
console.log('š SUMMARY STATISTICS\n');
const inProgress = byState['In Progress']?.length || 0;
const todo = byState['Todo']?.length || 0;
const triage = byState['Triage']?.length || 0;
const backlog = byState['Backlog']?.length || 0;
console.log(` š In Progress: ${inProgress}`);
console.log(` š Todo: ${todo}`);
console.log(` š Triage: ${triage}`);
console.log(` š¦ Backlog: ${backlog}`);
console.log(` š Total Active: ${issues.length}`);
// High priority items
const urgent = issues.filter(i => i.priority === 1);
const high = issues.filter(i => i.priority === 2);
if (urgent.length > 0) {
console.log('\nšØ URGENT PRIORITY TASKS:');
urgent.forEach(issue => {
const state = issue.state.name;
const assignee = issue.assignee?.name || 'Unassigned';
console.log(` ⢠${issue.identifier}: ${issue.title} [${state}] - ${assignee}`);
});
}
if (high.length > 0) {
console.log('\nš„ HIGH PRIORITY TASKS:');
high.slice(0, 5).forEach(issue => {
const state = issue.state.name;
const assignee = issue.assignee?.name || 'Unassigned';
console.log(` ⢠${issue.identifier}: ${issue.title} [${state}] - ${assignee}`);
});
}
// Recommendations
console.log('\n' + '='.repeat(50));
console.log('š” RECOMMENDED NEXT ACTIONS:\n');
// Find tasks to work on
const recommendations = [];
// 1. In-progress tasks that need completion
const myInProgress = issues.filter(i =>
i.state.name === 'In Progress' &&
(!i.assignee || i.assignee.name === viewerData.viewer.name)
);
if (myInProgress.length > 0) {
recommendations.push({
reason: 'š Continue in-progress work',
tasks: myInProgress.slice(0, 2)
});
}
// 2. High priority unassigned todos
const highPriorityTodos = issues.filter(i =>
i.state.name === 'Todo' &&
i.priority <= 2 &&
!i.assignee
);
if (highPriorityTodos.length > 0) {
recommendations.push({
reason: 'š„ High priority unassigned tasks',
tasks: highPriorityTodos.slice(0, 2)
});
}
// 3. Any todo tasks
const todoTasks = issues.filter(i =>
i.state.name === 'Todo' &&
!i.assignee
);
if (todoTasks.length > 0 && recommendations.length < 2) {
recommendations.push({
reason: 'š Available todo tasks',
tasks: todoTasks.slice(0, 2)
});
}
if (recommendations.length > 0) {
recommendations.forEach(rec => {
console.log(rec.reason);
rec.tasks.forEach(task => {
const priority = priorityLabels[task.priority] || 'āŖ None';
console.log(` āā ${task.identifier}: ${task.title}`);
console.log(` ${priority} | ${task.url}`);
});
console.log('');
});
} else {
console.log('ā
No immediate tasks requiring attention');
}
} catch (error) {
console.error('ā Error:', error.message);
process.exit(1);
}
}
reviewTasks();