@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.
186 lines (158 loc) ⢠5.92 kB
JavaScript
import { LinearClient } from '@linear/sdk';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Load environment variables from .env file
dotenv.config({ path: path.join(__dirname, '..', '.env') });
async function syncLinearTasks() {
const apiKey = process.env.STACKMEMORY_LINEAR_API_KEY || process.env.LINEAR_API_KEY;
if (!apiKey) {
console.error('ā LINEAR_API_KEY not found in environment');
process.exit(1);
}
console.log('š Connecting to Linear with API key...');
// Linear SDK expects the API key without the "lin_api_" prefix in some versions
// But let's use the full key and pass it correctly
const linearClient = new LinearClient({
apiKey: apiKey,
headers: {
'Authorization': apiKey
}
});
try {
// Test connection and get team info
console.log('š Fetching workspace info...');
const me = await linearClient.viewer;
console.log(`ā
Connected as: ${me.name || me.email}`);
// Get all teams
const teams = await linearClient.teams();
console.log(`\nš Found ${teams.nodes.length} teams:`);
for (const team of teams.nodes) {
console.log(` - ${team.key}: ${team.name}`);
}
// Get issues from StackMemory team
const stackTeam = teams.nodes.find(t => t.key === 'STA' || t.key === 'STACK');
if (!stackTeam) {
console.log('\nā ļø No StackMemory team found (looking for STA or STACK)');
return;
}
console.log(`\nš„ Fetching issues from ${stackTeam.name} (${stackTeam.key})...`);
const issues = await linearClient.issues({
filter: {
team: { key: { eq: stackTeam.key } }
},
first: 100
});
console.log(`Found ${issues.nodes.length} issues\n`);
// Load local tasks
const tasksFile = path.join(__dirname, '..', '.stackmemory', 'tasks.jsonl');
const localTasks = [];
if (fs.existsSync(tasksFile)) {
const lines = fs.readFileSync(tasksFile, 'utf8').split('\n').filter(l => l.trim());
for (const line of lines) {
try {
const task = JSON.parse(line);
if (task.type === 'task_create' || task.type === 'task_update') {
localTasks.push(task);
}
} catch (e) {
// Skip invalid lines
}
}
}
console.log(`š Local tasks: ${localTasks.length}`);
console.log(`š Linear issues: ${issues.nodes.length}`);
// Find tasks that exist in Linear but not locally
const localLinearIds = new Set();
for (const task of localTasks) {
const match = task.title?.match(/\[(STA-\d+|ENG-\d+)\]/);
if (match) {
localLinearIds.add(match[1]);
}
}
const missingLocally = [];
for (const issue of issues.nodes) {
if (!localLinearIds.has(issue.identifier)) {
missingLocally.push(issue);
}
}
if (missingLocally.length > 0) {
console.log(`\nš Issues in Linear but not in local tasks (${missingLocally.length}):`);
// Create task entries for missing issues
const newTasks = [];
for (const issue of missingLocally) {
console.log(` - ${issue.identifier}: ${issue.title}`);
const taskId = `tsk-${Math.random().toString(36).substr(2, 8)}`;
const task = {
id: taskId,
type: 'task_create',
timestamp: Date.now(),
frame_id: 'linear-sync',
title: `[${issue.identifier}] ${issue.title}`,
description: issue.description || '',
status: mapLinearState(issue.state.type),
priority: mapLinearPriority(issue.priority),
created_at: Date.now(),
depends_on: [],
blocks: [],
tags: ['linear', 'synced'],
context_score: 0.5,
external_refs: {
linear_id: issue.id,
linear_url: issue.url
}
};
newTasks.push(JSON.stringify(task));
}
if (newTasks.length > 0) {
console.log(`\nš¾ Adding ${newTasks.length} tasks to local storage...`);
fs.appendFileSync(tasksFile, '\n' + newTasks.join('\n') + '\n');
console.log('ā
Tasks synced successfully!');
}
} else {
console.log('\nā
All Linear issues already exist locally');
}
// Find local tasks not in Linear
const linearIds = new Set(issues.nodes.map(i => i.identifier));
const missingInLinear = localTasks.filter(task => {
const match = task.title?.match(/\[(STA-\d+|ENG-\d+)\]/);
return match && !linearIds.has(match[1]);
});
if (missingInLinear.length > 0) {
console.log(`\nš¤ Local tasks not in Linear (${missingInLinear.length}):`);
for (const task of missingInLinear.slice(0, 10)) {
console.log(` - ${task.title}`);
}
if (missingInLinear.length > 10) {
console.log(` ... and ${missingInLinear.length - 10} more`);
}
}
} catch (error) {
console.error('ā Error syncing with Linear:', error.message);
if (error.message.includes('authentication') || error.message.includes('401')) {
console.log('\nš Authentication failed. Please check your LINEAR_API_KEY');
console.log('You can get an API key from: https://linear.app/settings/api');
}
}
}
function mapLinearState(state) {
switch (state) {
case 'completed': return 'completed';
case 'started': return 'in_progress';
case 'cancelled': return 'cancelled';
default: return 'pending';
}
}
function mapLinearPriority(priority) {
switch (priority) {
case 1: return 'urgent';
case 2: return 'high';
case 3: return 'medium';
case 4: return 'low';
default: return 'medium';
}
}
syncLinearTasks();