@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.
201 lines (170 loc) ⢠7.46 kB
JavaScript
/**
* Delete duplicate STA tasks from Linear to free up capacity
*/
import 'dotenv/config';
import fs from 'fs';
import readline from 'readline';
// Load API key from environment
const API_KEY = process.env.LINEAR_API_KEY;
if (!API_KEY) {
console.error('ā LINEAR_API_KEY environment variable not set');
console.log('Please set LINEAR_API_KEY in your .env file or export it in your shell');
process.exit(1);
}
const BATCH_SIZE = 10; // Delete in batches to avoid rate limits
const DELAY_BETWEEN_BATCHES = 3000; // 3 seconds between batches
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
async function deleteSTATasks() {
try {
// Load deletion list
const deleteFile = `sta-deletion-list-${new Date().toISOString().split('T')[0]}.json`;
if (!fs.existsSync(deleteFile)) {
console.error(`ā Deletion list not found: ${deleteFile}`);
console.log('Run analyze-sta-graphql.js first to generate the list.');
process.exit(1);
}
const deleteList = JSON.parse(fs.readFileSync(deleteFile, 'utf8'));
console.log('š STA TASK DELETION SUMMARY');
console.log('=' .repeat(60));
console.log(`\n Total workspace tasks: ${deleteList.summary.totalWorkspace}`);
console.log(` Current STA tasks: ${deleteList.summary.totalSTA}`);
console.log(` Tasks to delete: ${deleteList.summary.toDelete}`);
console.log(` Workspace after deletion: ${deleteList.summary.remainingTotal}`);
console.log(`\n šÆ Capacity to be freed: ${deleteList.summary.capacityFreed} task slots\n`);
// Show breakdown
console.log('š Tasks to delete by category:');
Object.entries(deleteList.categories).forEach(([category, tasks]) => {
if (tasks.length > 0) {
console.log(` ⢠${category}: ${tasks.length} tasks`);
}
});
// Safety confirmation
console.log('\nā ļø WARNING: This will permanently delete ${deleteList.summary.toDelete} tasks from Linear!');
console.log('This action cannot be undone. Make sure you have reviewed the list.\n');
console.log('Type "DELETE STA TASKS" to confirm, or press Ctrl+C to cancel:\n');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const confirmation = await new Promise(resolve => {
rl.question('Confirmation: ', resolve);
});
rl.close();
if (confirmation !== 'DELETE STA TASKS') {
console.log('ā Deletion cancelled');
process.exit(0);
}
console.log('\nšļø Starting deletion process...');
console.log(`Will delete in batches of ${BATCH_SIZE} with ${DELAY_BETWEEN_BATCHES/1000}s delay\n`);
let deleted = 0;
let failed = 0;
let alreadyDeleted = 0;
const results = [];
const errors = [];
// Process in batches
for (let i = 0; i < deleteList.tasks.length; i += BATCH_SIZE) {
const batch = deleteList.tasks.slice(i, i + BATCH_SIZE);
console.log(`\nš¦ Batch ${Math.floor(i/BATCH_SIZE) + 1}/${Math.ceil(deleteList.tasks.length/BATCH_SIZE)}`);
for (const task of batch) {
try {
process.stdout.write(` Deleting ${task.identifier}: ${task.title.substring(0, 40)}... `);
const deleteQuery = `
mutation DeleteIssue($id: String!) {
issueDelete(id: $id) {
success
}
}
`;
const response = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
'Authorization': API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: deleteQuery,
variables: { id: task.id }
})
});
const result = await response.json();
if (result.data?.issueDelete?.success) {
console.log('ā
');
deleted++;
results.push({ ...task, status: 'deleted' });
} else if (result.errors?.[0]?.message?.includes('not found')) {
console.log('ā ļø (already gone)');
alreadyDeleted++;
results.push({ ...task, status: 'already_deleted' });
} else {
const error = result.errors?.[0]?.message || 'Unknown error';
console.log(`ā (${error})`);
failed++;
errors.push({ ...task, error });
results.push({ ...task, status: 'failed', error });
}
} catch (error) {
console.log(`ā (${error.message})`);
failed++;
errors.push({ ...task, error: error.message });
results.push({ ...task, status: 'error', error: error.message });
// If rate limited, wait longer
if (error.message?.includes('rate') || error.message?.includes('429')) {
console.log('ā³ Rate limited. Waiting 30 seconds...');
await delay(30000);
}
}
}
// Progress update
const total = deleted + alreadyDeleted + failed;
const percent = Math.round((total / deleteList.tasks.length) * 100);
console.log(`\nš Progress: ${total}/${deleteList.tasks.length} (${percent}%)`);
console.log(` ā
Deleted: ${deleted} | ā ļø Already gone: ${alreadyDeleted} | ā Failed: ${failed}`);
// Delay before next batch
if (i + BATCH_SIZE < deleteList.tasks.length) {
console.log(`ā³ Waiting ${DELAY_BETWEEN_BATCHES/1000}s before next batch...`);
await delay(DELAY_BETWEEN_BATCHES);
}
}
// Save results
const resultsFile = `sta-deletion-results-${new Date().toISOString().split('T')[0]}.json`;
fs.writeFileSync(resultsFile, JSON.stringify({
summary: {
attempted: deleteList.tasks.length,
deleted,
alreadyDeleted,
failed,
success_rate: Math.round(((deleted + alreadyDeleted) / deleteList.tasks.length) * 100)
},
results,
errors: errors.length > 0 ? errors : undefined,
timestamp: new Date().toISOString()
}, null, 2));
// Final report
console.log('\n' + '='.repeat(60));
console.log('š DELETION COMPLETE');
console.log('='.repeat(60));
console.log(`\nā
Successfully deleted: ${deleted} tasks`);
console.log(`ā ļø Already deleted: ${alreadyDeleted} tasks`);
console.log(`ā Failed: ${failed} tasks`);
console.log(`š Success rate: ${Math.round(((deleted + alreadyDeleted) / deleteList.tasks.length) * 100)}%`);
console.log(`\nš¾ Results saved to: ${resultsFile}`);
const totalFreed = deleted + alreadyDeleted;
if (totalFreed > 0) {
console.log(`\nš Freed up ${totalFreed} task slots in your Linear workspace!`);
console.log('\nYour workspace now has capacity for new tasks.');
console.log('The backlog has been cleaned of duplicates and similar tasks.');
}
if (failed > 0) {
console.log(`\nā ļø ${failed} tasks could not be deleted. Check ${resultsFile} for details.`);
}
} catch (error) {
console.error('\nš„ Script failed:', error.message);
process.exit(1);
}
}
// Run if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
deleteSTATasks().catch(console.error);
}
export { deleteSTATasks };