@stackmemoryai/stackmemory 0.3.16 → 0.3.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/README.md +48 -2
  2. package/dist/cli/commands/skills.js +15 -2
  3. package/dist/cli/commands/skills.js.map +2 -2
  4. package/dist/cli/index.js +113 -834
  5. package/dist/cli/index.js.map +3 -3
  6. package/dist/core/context/dual-stack-manager.js +1 -1
  7. package/dist/core/context/dual-stack-manager.js.map +1 -1
  8. package/dist/core/context/frame-manager.js +3 -0
  9. package/dist/core/context/frame-manager.js.map +2 -2
  10. package/dist/integrations/claude-code/subagent-client.js +106 -3
  11. package/dist/integrations/claude-code/subagent-client.js.map +2 -2
  12. package/dist/servers/railway/config.js +51 -0
  13. package/dist/servers/railway/config.js.map +7 -0
  14. package/dist/servers/railway/index-enhanced.js +156 -0
  15. package/dist/servers/railway/index-enhanced.js.map +7 -0
  16. package/dist/servers/railway/minimal.js +48 -3
  17. package/dist/servers/railway/minimal.js.map +2 -2
  18. package/dist/servers/railway/storage-test.js +455 -0
  19. package/dist/servers/railway/storage-test.js.map +7 -0
  20. package/dist/skills/claude-skills.js +13 -12
  21. package/dist/skills/claude-skills.js.map +2 -2
  22. package/dist/skills/recursive-agent-orchestrator.js +27 -18
  23. package/dist/skills/recursive-agent-orchestrator.js.map +2 -2
  24. package/dist/skills/unified-rlm-orchestrator.js.map +2 -2
  25. package/package.json +6 -18
  26. package/scripts/README-TESTING.md +186 -0
  27. package/scripts/analyze-cli-security.js +288 -0
  28. package/scripts/archive/add-phase-tasks-to-linear.js +163 -0
  29. package/scripts/archive/analyze-linear-duplicates.js +214 -0
  30. package/scripts/archive/analyze-remaining-duplicates.js +230 -0
  31. package/scripts/archive/analyze-sta-duplicates.js +292 -0
  32. package/scripts/archive/analyze-sta-graphql.js +399 -0
  33. package/scripts/archive/cancel-duplicate-tasks.ts +246 -0
  34. package/scripts/archive/check-all-duplicates.ts +419 -0
  35. package/scripts/archive/clean-duplicate-tasks.js +114 -0
  36. package/scripts/archive/cleanup-duplicate-tasks.ts +286 -0
  37. package/scripts/archive/create-phase-tasks.js +387 -0
  38. package/scripts/archive/delete-linear-duplicates.js +182 -0
  39. package/scripts/archive/delete-remaining-duplicates.js +158 -0
  40. package/scripts/archive/delete-sta-duplicates.js +201 -0
  41. package/scripts/archive/delete-sta-oauth.js +201 -0
  42. package/scripts/archive/export-sta-tasks.js +62 -0
  43. package/scripts/archive/install-auto-sync.js +266 -0
  44. package/scripts/archive/install-chromadb-hooks.sh +133 -0
  45. package/scripts/archive/install-enhanced-clear-hooks.sh +431 -0
  46. package/scripts/archive/install-post-task-hooks.sh +289 -0
  47. package/scripts/archive/install-stackmemory-hooks.sh +420 -0
  48. package/scripts/archive/merge-linear-duplicates-safe.ts +362 -0
  49. package/scripts/archive/merge-linear-duplicates.ts +180 -0
  50. package/scripts/archive/remove-sta-tasks.js +70 -0
  51. package/scripts/archive/setup-background-sync.sh +168 -0
  52. package/scripts/archive/setup-claude-auto-triggers.sh +181 -0
  53. package/scripts/archive/setup-claude-autostart.sh +305 -0
  54. package/scripts/archive/setup-git-hooks.sh +25 -0
  55. package/scripts/archive/setup-linear-oauth.sh +46 -0
  56. package/scripts/archive/setup-mcp.sh +113 -0
  57. package/scripts/archive/setup-railway-deployment.sh +81 -0
  58. package/scripts/auto-handoff.sh +262 -0
  59. package/scripts/background-sync-manager.js +416 -0
  60. package/scripts/benchmark-performance.ts +57 -0
  61. package/scripts/check-redis.ts +48 -0
  62. package/scripts/chromadb-auto-loader.sh +128 -0
  63. package/scripts/chromadb-context-loader.js +479 -0
  64. package/scripts/claude-chromadb-hook.js +460 -0
  65. package/scripts/claude-code-wrapper.sh +66 -0
  66. package/scripts/claude-linear-skill.js +455 -0
  67. package/scripts/claude-pre-commit.sh +302 -0
  68. package/scripts/claude-sm-autostart.js +532 -0
  69. package/scripts/claude-sm-setup.sh +367 -0
  70. package/scripts/claude-with-chromadb.sh +69 -0
  71. package/scripts/claude-worktree-manager.sh +323 -0
  72. package/scripts/claude-worktree-monitor.sh +371 -0
  73. package/scripts/claude-worktree-setup.sh +327 -0
  74. package/scripts/clean-linear-backlog.js +273 -0
  75. package/scripts/cleanup-old-sessions.sh +57 -0
  76. package/scripts/codex-wrapper.sh +88 -0
  77. package/scripts/create-sandbox.sh +269 -0
  78. package/scripts/debug-linear-update.js +174 -0
  79. package/scripts/delete-linear-tasks.js +167 -0
  80. package/scripts/deploy.sh +89 -0
  81. package/scripts/deployment/railway.sh +352 -0
  82. package/scripts/deployment/test-deployment.js +194 -0
  83. package/scripts/detect-and-rehydrate.js +162 -0
  84. package/scripts/detect-and-rehydrate.mjs +165 -0
  85. package/scripts/development/create-demo-tasks.js +143 -0
  86. package/scripts/development/debug-frame-test.js +16 -0
  87. package/scripts/development/demo-auto-sync.js +128 -0
  88. package/scripts/development/fix-all-imports.js +213 -0
  89. package/scripts/development/fix-imports.js +229 -0
  90. package/scripts/development/fix-lint-loop.cjs +103 -0
  91. package/scripts/development/fix-project-id.ts +161 -0
  92. package/scripts/development/fix-strict-mode-issues.ts +291 -0
  93. package/scripts/development/reorganize-structure.sh +228 -0
  94. package/scripts/development/test-persistence-direct.js +148 -0
  95. package/scripts/development/test-persistence.js +114 -0
  96. package/scripts/development/test-tasks.js +93 -0
  97. package/scripts/development/update-imports.js +212 -0
  98. package/scripts/fetch-linear-status.js +125 -0
  99. package/scripts/git-hooks/README.md +310 -0
  100. package/scripts/git-hooks/branch-context-manager.sh +342 -0
  101. package/scripts/git-hooks/post-checkout-stackmemory.sh +63 -0
  102. package/scripts/git-hooks/post-commit-stackmemory.sh +305 -0
  103. package/scripts/git-hooks/pre-commit-stackmemory.sh +275 -0
  104. package/scripts/hooks/cleanup-shell.sh +130 -0
  105. package/scripts/hooks/task-complete.sh +114 -0
  106. package/scripts/initialize.ts +129 -0
  107. package/scripts/install-claude-hooks-auto.js +104 -0
  108. package/scripts/install-claude-hooks.sh +133 -0
  109. package/scripts/install-global.sh +296 -0
  110. package/scripts/install.sh +235 -0
  111. package/scripts/linear-auto-sync.js +262 -0
  112. package/scripts/linear-auto-sync.sh +161 -0
  113. package/scripts/linear-sync-daemon.js +150 -0
  114. package/scripts/linear-task-review.js +237 -0
  115. package/scripts/list-linear-tasks.ts +178 -0
  116. package/scripts/mcp-proxy.js +66 -0
  117. package/scripts/opencode-wrapper.sh +85 -0
  118. package/scripts/publish-local.js +74 -0
  119. package/scripts/query-chromadb.ts +201 -0
  120. package/scripts/railway-env-setup.sh +39 -0
  121. package/scripts/reconcile-local-tasks.js +170 -0
  122. package/scripts/recreate-frames-db.js +89 -0
  123. package/scripts/setup/claude-integration.js +138 -0
  124. package/scripts/setup/configure-alias.js +125 -0
  125. package/scripts/setup/configure-codex-alias.js +161 -0
  126. package/scripts/setup/configure-opencode-alias.js +175 -0
  127. package/scripts/setup-claude-integration.js +204 -0
  128. package/scripts/setup-claude-integration.sh +183 -0
  129. package/scripts/setup.sh +31 -0
  130. package/scripts/show-linear-summary.ts +172 -0
  131. package/scripts/stackmemory-auto-handoff.sh +231 -0
  132. package/scripts/stackmemory-daemon.sh +40 -0
  133. package/scripts/start-linear-sync-daemon.sh +141 -0
  134. package/scripts/start-temporal-paradox.sh +214 -0
  135. package/scripts/status.ts +159 -0
  136. package/scripts/sync-and-clean-tasks.js +258 -0
  137. package/scripts/sync-frames-from-railway.js +228 -0
  138. package/scripts/sync-linear-graphql.js +303 -0
  139. package/scripts/sync-linear-tasks.js +186 -0
  140. package/scripts/test-auto-triggers.sh +57 -0
  141. package/scripts/test-browser-mcp.js +74 -0
  142. package/scripts/test-chromadb-full.js +115 -0
  143. package/scripts/test-chromadb-hooks.sh +28 -0
  144. package/scripts/test-chromadb-sync.ts +245 -0
  145. package/scripts/test-cli-security.js +293 -0
  146. package/scripts/test-hooks-persistence.sh +220 -0
  147. package/scripts/test-installation-scenarios.sh +359 -0
  148. package/scripts/test-installation.sh +224 -0
  149. package/scripts/test-mcp.js +163 -0
  150. package/scripts/test-pre-publish-quick.sh +75 -0
  151. package/scripts/test-quality-gates.sh +263 -0
  152. package/scripts/test-railway-db.js +222 -0
  153. package/scripts/test-redis-storage.ts +490 -0
  154. package/scripts/test-rlm-basic.sh +122 -0
  155. package/scripts/test-rlm-comprehensive.sh +260 -0
  156. package/scripts/test-rlm-e2e.sh +268 -0
  157. package/scripts/test-rlm-simple.js +90 -0
  158. package/scripts/test-rlm.js +110 -0
  159. package/scripts/test-session-handoff.sh +165 -0
  160. package/scripts/test-shell-integration.sh +275 -0
  161. package/scripts/testing/ab-test-runner.ts +508 -0
  162. package/scripts/testing/collect-metrics.ts +457 -0
  163. package/scripts/testing/quick-effectiveness-demo.js +187 -0
  164. package/scripts/testing/real-performance-test.js +422 -0
  165. package/scripts/testing/run-effectiveness-tests.sh +176 -0
  166. package/scripts/testing/scripts/testing/ab-test-runner.js +363 -0
  167. package/scripts/testing/scripts/testing/collect-metrics.js +292 -0
  168. package/scripts/testing/simple-effectiveness-test.js +310 -0
  169. package/scripts/testing/src/core/context/context-bridge.js +253 -0
  170. package/scripts/testing/src/core/context/frame-manager.js +746 -0
  171. package/scripts/testing/src/core/context/shared-context-layer.js +437 -0
  172. package/scripts/testing/src/core/database/database-adapter.js +54 -0
  173. package/scripts/testing/src/core/errors/index.js +291 -0
  174. package/scripts/testing/src/core/errors/recovery.js +268 -0
  175. package/scripts/testing/src/core/monitoring/logger.js +145 -0
  176. package/scripts/testing/src/core/retrieval/context-retriever.js +516 -0
  177. package/scripts/testing/src/core/session/index.js +1 -0
  178. package/scripts/testing/src/core/session/session-manager.js +323 -0
  179. package/scripts/testing/src/core/trace/cli-trace-wrapper.js +140 -0
  180. package/scripts/testing/src/core/trace/db-trace-wrapper.js +251 -0
  181. package/scripts/testing/src/core/trace/debug-trace.js +398 -0
  182. package/scripts/testing/src/core/trace/index.js +120 -0
  183. package/scripts/testing/src/core/trace/linear-api-wrapper.js +204 -0
  184. package/scripts/update-linear-status.js +268 -0
  185. package/scripts/update-linear-tasks-fixed.js +284 -0
  186. package/templates/claude-hooks/hooks.json +5 -0
  187. package/templates/claude-hooks/on-clear.js +56 -0
  188. package/templates/claude-hooks/on-startup.js +56 -0
  189. package/templates/claude-hooks/tool-use-trace.js +67 -0
  190. package/dist/features/tui/components/analytics-panel.js +0 -157
  191. package/dist/features/tui/components/analytics-panel.js.map +0 -7
  192. package/dist/features/tui/components/frame-visualizer.js +0 -377
  193. package/dist/features/tui/components/frame-visualizer.js.map +0 -7
  194. package/dist/features/tui/components/pr-tracker.js +0 -135
  195. package/dist/features/tui/components/pr-tracker.js.map +0 -7
  196. package/dist/features/tui/components/session-monitor.js +0 -299
  197. package/dist/features/tui/components/session-monitor.js.map +0 -7
  198. package/dist/features/tui/components/subagent-fleet.js +0 -395
  199. package/dist/features/tui/components/subagent-fleet.js.map +0 -7
  200. package/dist/features/tui/components/task-board.js +0 -1139
  201. package/dist/features/tui/components/task-board.js.map +0 -7
  202. package/dist/features/tui/index.js +0 -408
  203. package/dist/features/tui/index.js.map +0 -7
  204. package/dist/features/tui/services/data-service.js +0 -641
  205. package/dist/features/tui/services/data-service.js.map +0 -7
  206. package/dist/features/tui/services/linear-task-reader.js +0 -102
  207. package/dist/features/tui/services/linear-task-reader.js.map +0 -7
  208. package/dist/features/tui/services/websocket-client.js +0 -162
  209. package/dist/features/tui/services/websocket-client.js.map +0 -7
  210. package/dist/features/tui/terminal-compat.js +0 -220
  211. package/dist/features/tui/terminal-compat.js.map +0 -7
  212. package/dist/features/tui/types.js +0 -1
  213. package/dist/features/tui/types.js.map +0 -7
@@ -0,0 +1,399 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Analyze Linear workspace for STA duplicates using GraphQL API directly
5
+ */
6
+
7
+ import 'dotenv/config';
8
+ import fs from 'fs';
9
+
10
+ // Load API key from environment
11
+ const API_KEY = process.env.LINEAR_API_KEY;
12
+ if (!API_KEY) {
13
+ console.error('āŒ LINEAR_API_KEY environment variable not set');
14
+ console.log('Please set LINEAR_API_KEY in your .env file or export it in your shell');
15
+ process.exit(1);
16
+ }
17
+
18
+ async function fetchAllIssues() {
19
+ const query = `
20
+ query GetAllIssues($after: String) {
21
+ issues(first: 100, after: $after, includeArchived: false) {
22
+ nodes {
23
+ id
24
+ identifier
25
+ title
26
+ description
27
+ state {
28
+ id
29
+ name
30
+ type
31
+ }
32
+ createdAt
33
+ updatedAt
34
+ priority
35
+ estimate
36
+ project {
37
+ id
38
+ name
39
+ }
40
+ team {
41
+ id
42
+ key
43
+ name
44
+ }
45
+ }
46
+ pageInfo {
47
+ hasNextPage
48
+ endCursor
49
+ }
50
+ }
51
+ }
52
+ `;
53
+
54
+ let allIssues = [];
55
+ let hasNextPage = true;
56
+ let cursor = null;
57
+
58
+ while (hasNextPage) {
59
+ const response = await fetch('https://api.linear.app/graphql', {
60
+ method: 'POST',
61
+ headers: {
62
+ 'Authorization': API_KEY,
63
+ 'Content-Type': 'application/json'
64
+ },
65
+ body: JSON.stringify({
66
+ query,
67
+ variables: { after: cursor }
68
+ })
69
+ });
70
+
71
+ const result = await response.json();
72
+
73
+ if (result.errors) {
74
+ throw new Error(result.errors[0].message);
75
+ }
76
+
77
+ allIssues = allIssues.concat(result.data.issues.nodes);
78
+ hasNextPage = result.data.issues.pageInfo.hasNextPage;
79
+ cursor = result.data.issues.pageInfo.endCursor;
80
+
81
+ console.log(` Fetched ${allIssues.length} issues...`);
82
+ }
83
+
84
+ return allIssues;
85
+ }
86
+
87
+ async function analyzeSTADuplicates() {
88
+ try {
89
+ console.log('šŸ” Analyzing Linear workspace for STA duplicates and unneeded tasks...\n');
90
+ console.log('šŸ“„ Fetching all tasks from Linear (using GraphQL)...');
91
+
92
+ const allTasks = await fetchAllIssues();
93
+ console.log(`šŸ“Š Total tasks in workspace: ${allTasks.length}\n`);
94
+
95
+ // Filter STA tasks
96
+ const staTasks = allTasks.filter(task =>
97
+ task.identifier.startsWith('STA-') ||
98
+ task.title.includes('STA-') ||
99
+ task.title.includes('[STA-')
100
+ );
101
+
102
+ console.log(`šŸ“Œ Found ${staTasks.length} STA-related tasks\n`);
103
+
104
+ // Analyze patterns
105
+ const staByNumber = new Map();
106
+ const completedSTA = [];
107
+ const canceledSTA = [];
108
+ const duplicateTitles = new Map();
109
+ const lowValuePatterns = [];
110
+ const backlogSTA = [];
111
+ const todoSTA = [];
112
+ const inProgressSTA = [];
113
+
114
+ // Group tasks by patterns
115
+ staTasks.forEach(task => {
116
+ const state = task.state.type;
117
+ const status = task.state.name;
118
+
119
+ // Extract STA number
120
+ const staMatch = task.identifier.match(/STA-(\d+)/);
121
+ if (staMatch) {
122
+ const staNum = parseInt(staMatch[1]);
123
+ if (!staByNumber.has(staNum)) {
124
+ staByNumber.set(staNum, []);
125
+ }
126
+ staByNumber.get(staNum).push(task);
127
+ }
128
+
129
+ // Check state
130
+ if (state === 'completed') {
131
+ completedSTA.push(task);
132
+ } else if (state === 'canceled') {
133
+ canceledSTA.push(task);
134
+ } else if (state === 'backlog' || status === 'Backlog') {
135
+ backlogSTA.push(task);
136
+ } else if (state === 'started' || status === 'In Progress') {
137
+ inProgressSTA.push(task);
138
+ } else if (state === 'unstarted' || state === 'triage' || status === 'Todo') {
139
+ todoSTA.push(task);
140
+ }
141
+
142
+ // Check for low-value patterns
143
+ const lowValueKeywords = [
144
+ 'Documentation TODO',
145
+ 'Meeting',
146
+ 'Task Analytics Dashboard',
147
+ 'Weekly Sync',
148
+ 'Standup',
149
+ '[Duplicate]',
150
+ 'Test Task',
151
+ 'Demo Task',
152
+ 'Example Task'
153
+ ];
154
+
155
+ if (lowValueKeywords.some(keyword => task.title.includes(keyword)) ||
156
+ task.description?.includes('auto-generated') ||
157
+ task.description?.includes('automatically created')) {
158
+ lowValuePatterns.push(task);
159
+ }
160
+
161
+ // Find duplicate titles
162
+ const baseTitle = task.title
163
+ .replace(/\[.*?\]/g, '')
164
+ .replace(/STA-\d+/g, '')
165
+ .trim()
166
+ .toLowerCase();
167
+
168
+ if (baseTitle.length > 10) { // Only consider meaningful titles
169
+ if (!duplicateTitles.has(baseTitle)) {
170
+ duplicateTitles.set(baseTitle, []);
171
+ }
172
+ duplicateTitles.get(baseTitle).push(task);
173
+ }
174
+ });
175
+
176
+ // Find true duplicates
177
+ const duplicateSTANumbers = Array.from(staByNumber.entries())
178
+ .filter(([_, tasks]) => tasks.length > 1)
179
+ .sort((a, b) => b[1].length - a[1].length);
180
+
181
+ const trueDuplicateTitles = Array.from(duplicateTitles.entries())
182
+ .filter(([_, tasks]) => tasks.length > 1)
183
+ .sort((a, b) => b[1].length - a[1].length);
184
+
185
+ // Generate report
186
+ console.log('šŸ“‹ STA TASK ANALYSIS REPORT');
187
+ console.log('=' .repeat(60));
188
+
189
+ console.log('\nšŸ“Š OVERVIEW:');
190
+ console.log(`Total workspace tasks: ${allTasks.length}`);
191
+ console.log(`STA-prefixed tasks: ${staTasks.length} (${Math.round(staTasks.length / allTasks.length * 100)}% of workspace)`);
192
+ console.log(`\nStatus breakdown:`);
193
+ console.log(` • In Progress: ${inProgressSTA.length}`);
194
+ console.log(` • Todo/Ready: ${todoSTA.length}`);
195
+ console.log(` • Backlog: ${backlogSTA.length}`);
196
+ console.log(` • Completed: ${completedSTA.length}`);
197
+ console.log(` • Canceled: ${canceledSTA.length}`);
198
+
199
+ if (duplicateSTANumbers.length > 0) {
200
+ console.log('\nšŸ”„ DUPLICATE STA NUMBERS:');
201
+ console.log(`Found ${duplicateSTANumbers.length} STA numbers with multiple tasks:\n`);
202
+ duplicateSTANumbers.forEach(([staNum, tasks]) => {
203
+ console.log(` STA-${staNum} has ${tasks.length} instances:`);
204
+ tasks.forEach(task => {
205
+ console.log(` • ${task.identifier}: "${task.title.substring(0, 60)}..." (${task.state.name})`);
206
+ });
207
+ });
208
+ }
209
+
210
+ if (trueDuplicateTitles.length > 0) {
211
+ console.log('\nšŸ“ SIMILAR TITLES (potential duplicates):');
212
+ console.log(`Found ${trueDuplicateTitles.length} groups of similar titles:\n`);
213
+ trueDuplicateTitles.slice(0, 15).forEach(([title, tasks]) => {
214
+ console.log(` "${title.substring(0, 50)}..." appears ${tasks.length} times:`);
215
+ tasks.slice(0, 5).forEach(task => {
216
+ console.log(` • ${task.identifier}: ${task.state.name}`);
217
+ });
218
+ });
219
+ }
220
+
221
+ if (lowValuePatterns.length > 0) {
222
+ console.log('\nšŸ—‘ļø LOW-VALUE/AUTO-GENERATED TASKS:');
223
+ console.log(`Found ${lowValuePatterns.length} potentially low-value tasks:\n`);
224
+ const categories = {
225
+ 'Meeting/Sync tasks': lowValuePatterns.filter(t =>
226
+ t.title.includes('Meeting') || t.title.includes('Sync') || t.title.includes('Standup')),
227
+ 'Documentation TODOs': lowValuePatterns.filter(t =>
228
+ t.title.includes('Documentation TODO')),
229
+ 'Test/Demo tasks': lowValuePatterns.filter(t =>
230
+ t.title.includes('Test Task') || t.title.includes('Demo') || t.title.includes('Example')),
231
+ 'Auto-generated': lowValuePatterns.filter(t =>
232
+ t.description?.includes('auto-generated') || t.description?.includes('automatically'))
233
+ };
234
+
235
+ Object.entries(categories).forEach(([category, tasks]) => {
236
+ if (tasks.length > 0) {
237
+ console.log(` ${category}: ${tasks.length} tasks`);
238
+ tasks.slice(0, 5).forEach(task => {
239
+ console.log(` • ${task.identifier}: ${task.title.substring(0, 50)}...`);
240
+ });
241
+ }
242
+ });
243
+ }
244
+
245
+ // Build deletion recommendations
246
+ const toDelete = new Set();
247
+
248
+ // Add duplicates (keep most recent or in-progress)
249
+ duplicateSTANumbers.forEach(([_, tasks]) => {
250
+ const sorted = tasks.sort((a, b) => {
251
+ if (a.state.type === 'started') return -1;
252
+ if (b.state.type === 'started') return 1;
253
+ return new Date(b.updatedAt) - new Date(a.updatedAt);
254
+ });
255
+ sorted.slice(1).forEach(t => toDelete.add(t));
256
+ });
257
+
258
+ // Add similar titles (be more conservative)
259
+ trueDuplicateTitles.forEach(([_, tasks]) => {
260
+ if (tasks.every(t => t.state.type !== 'started')) {
261
+ // If none are in progress, keep newest
262
+ const sorted = tasks.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
263
+ sorted.slice(1).forEach(t => toDelete.add(t));
264
+ }
265
+ });
266
+
267
+ // Add completed and canceled
268
+ completedSTA.forEach(t => toDelete.add(t));
269
+ canceledSTA.forEach(t => toDelete.add(t));
270
+
271
+ // Add clear low-value patterns
272
+ lowValuePatterns.forEach(task => {
273
+ if (task.state.type !== 'started' &&
274
+ (task.title.includes('[Duplicate]') ||
275
+ task.title.includes('Test Task') ||
276
+ task.description?.includes('auto-generated'))) {
277
+ toDelete.add(task);
278
+ }
279
+ });
280
+
281
+ const uniqueToDelete = Array.from(toDelete)
282
+ .sort((a, b) => {
283
+ const aNum = parseInt(a.identifier.replace('STA-', ''));
284
+ const bNum = parseInt(b.identifier.replace('STA-', ''));
285
+ return aNum - bNum;
286
+ });
287
+
288
+ console.log('\nšŸŽÆ DELETION RECOMMENDATIONS');
289
+ console.log('=' .repeat(60));
290
+ console.log(`\nCapacity Analysis:`);
291
+ console.log(` Current workspace total: ${allTasks.length} tasks`);
292
+ console.log(` Current STA tasks: ${staTasks.length}`);
293
+ console.log(` Recommended to delete: ${uniqueToDelete.length} tasks`);
294
+ console.log(` Workspace after deletion: ${allTasks.length - uniqueToDelete.length} tasks`);
295
+ console.log(` STA tasks after deletion: ${staTasks.length - uniqueToDelete.length}`);
296
+ console.log(` **Capacity freed: ${uniqueToDelete.length} task slots**`);
297
+
298
+ // Categorize deletions
299
+ const deleteReasons = {
300
+ 'Duplicate STA numbers': [],
301
+ 'Similar/duplicate titles': [],
302
+ 'Completed tasks': [],
303
+ 'Canceled tasks': [],
304
+ 'Low-value/auto-generated': []
305
+ };
306
+
307
+ uniqueToDelete.forEach(task => {
308
+ let categorized = false;
309
+
310
+ // Check if it's a duplicate STA number
311
+ const staMatch = task.identifier.match(/STA-(\d+)/);
312
+ if (staMatch) {
313
+ const staNum = parseInt(staMatch[1]);
314
+ const dupes = staByNumber.get(staNum);
315
+ if (dupes && dupes.length > 1 && dupes[0].id !== task.id) {
316
+ deleteReasons['Duplicate STA numbers'].push(task);
317
+ categorized = true;
318
+ }
319
+ }
320
+
321
+ if (!categorized && completedSTA.find(t => t.id === task.id)) {
322
+ deleteReasons['Completed tasks'].push(task);
323
+ categorized = true;
324
+ }
325
+
326
+ if (!categorized && canceledSTA.find(t => t.id === task.id)) {
327
+ deleteReasons['Canceled tasks'].push(task);
328
+ categorized = true;
329
+ }
330
+
331
+ if (!categorized && lowValuePatterns.find(t => t.id === task.id)) {
332
+ deleteReasons['Low-value/auto-generated'].push(task);
333
+ categorized = true;
334
+ }
335
+
336
+ if (!categorized) {
337
+ deleteReasons['Similar/duplicate titles'].push(task);
338
+ }
339
+ });
340
+
341
+ console.log('\nšŸ“ Deletion breakdown by reason:');
342
+ Object.entries(deleteReasons).forEach(([reason, tasks]) => {
343
+ if (tasks.length > 0) {
344
+ console.log(`\n${reason}: ${tasks.length} tasks`);
345
+ const preview = tasks.slice(0, 10)
346
+ .map(t => `${t.identifier}`)
347
+ .join(', ');
348
+ console.log(` ${preview}${tasks.length > 10 ? ` ... +${tasks.length - 10} more` : ''}`);
349
+ }
350
+ });
351
+
352
+ // Save deletion list
353
+ const deleteList = {
354
+ timestamp: new Date().toISOString(),
355
+ summary: {
356
+ totalWorkspace: allTasks.length,
357
+ totalSTA: staTasks.length,
358
+ toDelete: uniqueToDelete.length,
359
+ capacityFreed: uniqueToDelete.length,
360
+ remainingSTA: staTasks.length - uniqueToDelete.length,
361
+ remainingTotal: allTasks.length - uniqueToDelete.length
362
+ },
363
+ categories: Object.entries(deleteReasons).reduce((acc, [reason, tasks]) => {
364
+ acc[reason] = tasks.map(t => t.identifier);
365
+ return acc;
366
+ }, {}),
367
+ tasks: uniqueToDelete.map(t => ({
368
+ id: t.id,
369
+ identifier: t.identifier,
370
+ title: t.title,
371
+ state: t.state.name,
372
+ team: t.team?.name || 'No team'
373
+ }))
374
+ };
375
+
376
+ const filename = `sta-deletion-list-${new Date().toISOString().split('T')[0]}.json`;
377
+ fs.writeFileSync(filename, JSON.stringify(deleteList, null, 2));
378
+
379
+ console.log(`\nšŸ’¾ Deletion list saved to: ${filename}`);
380
+ console.log('\nāœ… Ready to free up capacity!');
381
+ console.log('\nNext: Create a deletion script or manually review the JSON file');
382
+
383
+ return deleteList;
384
+
385
+ } catch (error) {
386
+ console.error('āŒ Analysis failed:', error.message);
387
+ if (error.message.includes('401') || error.message.includes('Unauthorized')) {
388
+ console.error('\nāš ļø Authentication failed. Please check your LINEAR_API_KEY');
389
+ }
390
+ process.exit(1);
391
+ }
392
+ }
393
+
394
+ // Run if called directly
395
+ if (import.meta.url === `file://${process.argv[1]}`) {
396
+ analyzeSTADuplicates().catch(console.error);
397
+ }
398
+
399
+ export { analyzeSTADuplicates };
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Script to cancel duplicate Linear tasks
5
+ * Uses the actual Linear API to find and cancel duplicates
6
+ */
7
+
8
+ import { readFileSync } from 'fs';
9
+ import { join } from 'path';
10
+
11
+ interface TaskGroup {
12
+ pattern: string;
13
+ keepFirst: boolean;
14
+ }
15
+
16
+ interface LinearIssue {
17
+ id: string;
18
+ identifier: string;
19
+ title: string;
20
+ createdAt: string;
21
+ team?: {
22
+ states?: {
23
+ nodes?: Array<{ id: string; type: string }>;
24
+ };
25
+ };
26
+ }
27
+
28
+ interface WorkflowState {
29
+ id: string;
30
+ type: string;
31
+ }
32
+
33
+ // Define patterns to identify duplicate tasks
34
+ const duplicatePatterns: TaskGroup[] = [
35
+ { pattern: 'Linear API Integration', keepFirst: true },
36
+ { pattern: 'Performance Optimization', keepFirst: true },
37
+ { pattern: 'Security Audit', keepFirst: true },
38
+ { pattern: '[HIGH] Implement Proper Error Handling', keepFirst: true },
39
+ { pattern: '[HIGH] Implement Comprehensive Testing Suite', keepFirst: true },
40
+ ];
41
+
42
+ async function cancelDuplicateTasks(dryRun = true) {
43
+ const mode = dryRun ? 'šŸ” DRY RUN MODE' : '⚔ LIVE MODE';
44
+ console.log(`\n${mode} - Cancel duplicate Linear tasks\n`);
45
+ console.log('='.repeat(60));
46
+
47
+ // Load Linear tokens
48
+ const tokensPath = join(process.cwd(), '.stackmemory', 'linear-tokens.json');
49
+ let accessToken: string;
50
+
51
+ try {
52
+ const tokensData = readFileSync(tokensPath, 'utf8');
53
+ const tokens = JSON.parse(tokensData);
54
+ accessToken = tokens.accessToken;
55
+ console.log('āœ… Loaded Linear authentication tokens\n');
56
+ } catch {
57
+ console.error(
58
+ 'āŒ Failed to load Linear tokens. Please run: stackmemory linear setup'
59
+ );
60
+ process.exit(1);
61
+ }
62
+
63
+ // GraphQL helper
64
+ const linearApiUrl = 'https://api.linear.app/graphql';
65
+
66
+ async function graphqlRequest(
67
+ query: string,
68
+ variables: Record<string, unknown> = {}
69
+ ) {
70
+ const response = await fetch(linearApiUrl, {
71
+ method: 'POST',
72
+ headers: {
73
+ Authorization: `Bearer ${accessToken}`,
74
+ 'Content-Type': 'application/json',
75
+ },
76
+ body: JSON.stringify({ query, variables }),
77
+ });
78
+
79
+ if (!response.ok) {
80
+ throw new Error(
81
+ `Linear API error: ${response.status} ${response.statusText}`
82
+ );
83
+ }
84
+
85
+ const result = (await response.json()) as {
86
+ errors?: unknown[];
87
+ data: unknown;
88
+ };
89
+ if (result.errors) {
90
+ throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`);
91
+ }
92
+
93
+ return result.data;
94
+ }
95
+
96
+ // First, get all issues
97
+ console.log('Fetching all issues...\n');
98
+ const issuesQuery = `
99
+ query {
100
+ issues(first: 250, filter: { state: { type: { nin: ["completed", "canceled"] } } }) {
101
+ nodes {
102
+ id
103
+ identifier
104
+ title
105
+ description
106
+ createdAt
107
+ state {
108
+ id
109
+ name
110
+ type
111
+ }
112
+ team {
113
+ id
114
+ key
115
+ states {
116
+ nodes {
117
+ id
118
+ name
119
+ type
120
+ }
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ `;
127
+
128
+ const issuesData = (await graphqlRequest(issuesQuery)) as {
129
+ issues: { nodes: LinearIssue[] };
130
+ };
131
+ const allIssues = issuesData.issues.nodes;
132
+
133
+ console.log(`Found ${allIssues.length} active issues\n`);
134
+
135
+ // Get canceled state from the first issue's team
136
+ const canceledState = allIssues[0]?.team?.states?.nodes?.find(
137
+ (s: WorkflowState) => s.type === 'canceled'
138
+ );
139
+ if (!canceledState) {
140
+ console.error('āŒ No canceled state found in workflow');
141
+ process.exit(1);
142
+ }
143
+
144
+ // Group issues by pattern
145
+ const groupedIssues = new Map<string, LinearIssue[]>();
146
+
147
+ for (const pattern of duplicatePatterns) {
148
+ const matches = allIssues.filter((issue: LinearIssue) =>
149
+ issue.title.includes(pattern.pattern)
150
+ );
151
+
152
+ if (matches.length > 1) {
153
+ // Sort by creation date to keep the oldest
154
+ matches.sort(
155
+ (a: LinearIssue, b: LinearIssue) =>
156
+ new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
157
+ );
158
+ groupedIssues.set(pattern.pattern, matches);
159
+ }
160
+ }
161
+
162
+ // Process each group
163
+ let totalCanceled = 0;
164
+ let totalKept = 0;
165
+
166
+ for (const [pattern, issues] of groupedIssues.entries()) {
167
+ console.log(`\nšŸ“‹ Pattern: "${pattern}"`);
168
+ console.log(` Found ${issues.length} matching issues:`);
169
+
170
+ const [primary, ...duplicates] = issues;
171
+
172
+ console.log(` āœ… Keep: ${primary.identifier} - ${primary.title}`);
173
+ totalKept++;
174
+
175
+ for (const duplicate of duplicates) {
176
+ console.log(
177
+ ` ${dryRun ? 'šŸ”' : 'āŒ'} Cancel: ${duplicate.identifier} - ${duplicate.title}`
178
+ );
179
+
180
+ if (!dryRun) {
181
+ try {
182
+ const cancelMutation = `
183
+ mutation CancelIssue($id: String!, $stateId: String!) {
184
+ issueUpdate(
185
+ id: $id,
186
+ input: {
187
+ stateId: $stateId,
188
+ description: "Duplicate task - kept ${primary.identifier}"
189
+ }
190
+ ) {
191
+ success
192
+ issue {
193
+ identifier
194
+ state {
195
+ name
196
+ }
197
+ }
198
+ }
199
+ }
200
+ `;
201
+
202
+ await graphqlRequest(cancelMutation, {
203
+ id: duplicate.id,
204
+ stateId: canceledState.id,
205
+ });
206
+
207
+ console.log(` āœ… Successfully canceled ${duplicate.identifier}`);
208
+ totalCanceled++;
209
+ } catch (error: unknown) {
210
+ const errorMessage =
211
+ error instanceof Error ? error.message : String(error);
212
+ console.log(
213
+ ` āŒ Failed to cancel ${duplicate.identifier}: ${errorMessage}`
214
+ );
215
+ }
216
+ } else {
217
+ totalCanceled++;
218
+ }
219
+ }
220
+ }
221
+
222
+ // Summary
223
+ console.log('\n' + '='.repeat(60));
224
+ console.log(`\n✨ ${dryRun ? 'DRY RUN' : 'CLEANUP'} COMPLETE!\n`);
225
+ console.log('šŸ“Š Summary:');
226
+ console.log(` Duplicate groups found: ${groupedIssues.size}`);
227
+ console.log(` Tasks to keep: ${totalKept}`);
228
+ console.log(
229
+ ` Tasks ${dryRun ? 'to cancel' : 'canceled'}: ${totalCanceled}`
230
+ );
231
+ console.log(` Total active tasks: ${allIssues.length}`);
232
+ console.log(` Tasks after cleanup: ${allIssues.length - totalCanceled}`);
233
+
234
+ if (dryRun) {
235
+ console.log('\nšŸ’” To execute these changes, run with --execute flag');
236
+ }
237
+ }
238
+
239
+ // Parse command line arguments
240
+ const isDryRun = !process.argv.includes('--execute');
241
+
242
+ // Run the cleanup
243
+ cancelDuplicateTasks(isDryRun).catch((error) => {
244
+ console.error('āŒ Fatal error:', error);
245
+ process.exit(1);
246
+ });