@stackmemoryai/stackmemory 0.3.17 → 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.
- package/dist/cli/commands/skills.js +15 -2
- package/dist/cli/commands/skills.js.map +2 -2
- package/dist/cli/index.js +113 -834
- package/dist/cli/index.js.map +3 -3
- package/dist/core/context/dual-stack-manager.js +1 -1
- package/dist/core/context/dual-stack-manager.js.map +1 -1
- package/dist/core/context/frame-manager.js +3 -0
- package/dist/core/context/frame-manager.js.map +2 -2
- package/dist/integrations/claude-code/subagent-client.js +106 -3
- package/dist/integrations/claude-code/subagent-client.js.map +2 -2
- package/dist/servers/railway/config.js +51 -0
- package/dist/servers/railway/config.js.map +7 -0
- package/dist/servers/railway/index-enhanced.js +156 -0
- package/dist/servers/railway/index-enhanced.js.map +7 -0
- package/dist/servers/railway/minimal.js +48 -3
- package/dist/servers/railway/minimal.js.map +2 -2
- package/dist/servers/railway/storage-test.js +455 -0
- package/dist/servers/railway/storage-test.js.map +7 -0
- package/dist/skills/claude-skills.js +13 -12
- package/dist/skills/claude-skills.js.map +2 -2
- package/dist/skills/recursive-agent-orchestrator.js +27 -18
- package/dist/skills/recursive-agent-orchestrator.js.map +2 -2
- package/dist/skills/unified-rlm-orchestrator.js.map +2 -2
- package/package.json +6 -18
- package/scripts/README-TESTING.md +186 -0
- package/scripts/analyze-cli-security.js +288 -0
- package/scripts/archive/add-phase-tasks-to-linear.js +163 -0
- package/scripts/archive/analyze-linear-duplicates.js +214 -0
- package/scripts/archive/analyze-remaining-duplicates.js +230 -0
- package/scripts/archive/analyze-sta-duplicates.js +292 -0
- package/scripts/archive/analyze-sta-graphql.js +399 -0
- package/scripts/archive/cancel-duplicate-tasks.ts +246 -0
- package/scripts/archive/check-all-duplicates.ts +419 -0
- package/scripts/archive/clean-duplicate-tasks.js +114 -0
- package/scripts/archive/cleanup-duplicate-tasks.ts +286 -0
- package/scripts/archive/create-phase-tasks.js +387 -0
- package/scripts/archive/delete-linear-duplicates.js +182 -0
- package/scripts/archive/delete-remaining-duplicates.js +158 -0
- package/scripts/archive/delete-sta-duplicates.js +201 -0
- package/scripts/archive/delete-sta-oauth.js +201 -0
- package/scripts/archive/export-sta-tasks.js +62 -0
- package/scripts/archive/install-auto-sync.js +266 -0
- package/scripts/archive/install-chromadb-hooks.sh +133 -0
- package/scripts/archive/install-enhanced-clear-hooks.sh +431 -0
- package/scripts/archive/install-post-task-hooks.sh +289 -0
- package/scripts/archive/install-stackmemory-hooks.sh +420 -0
- package/scripts/archive/merge-linear-duplicates-safe.ts +362 -0
- package/scripts/archive/merge-linear-duplicates.ts +180 -0
- package/scripts/archive/remove-sta-tasks.js +70 -0
- package/scripts/archive/setup-background-sync.sh +168 -0
- package/scripts/archive/setup-claude-auto-triggers.sh +181 -0
- package/scripts/archive/setup-claude-autostart.sh +305 -0
- package/scripts/archive/setup-git-hooks.sh +25 -0
- package/scripts/archive/setup-linear-oauth.sh +46 -0
- package/scripts/archive/setup-mcp.sh +113 -0
- package/scripts/archive/setup-railway-deployment.sh +81 -0
- package/scripts/auto-handoff.sh +262 -0
- package/scripts/background-sync-manager.js +416 -0
- package/scripts/benchmark-performance.ts +57 -0
- package/scripts/check-redis.ts +48 -0
- package/scripts/chromadb-auto-loader.sh +128 -0
- package/scripts/chromadb-context-loader.js +479 -0
- package/scripts/claude-chromadb-hook.js +460 -0
- package/scripts/claude-code-wrapper.sh +66 -0
- package/scripts/claude-linear-skill.js +455 -0
- package/scripts/claude-pre-commit.sh +302 -0
- package/scripts/claude-sm-autostart.js +532 -0
- package/scripts/claude-sm-setup.sh +367 -0
- package/scripts/claude-with-chromadb.sh +69 -0
- package/scripts/claude-worktree-manager.sh +323 -0
- package/scripts/claude-worktree-monitor.sh +371 -0
- package/scripts/claude-worktree-setup.sh +327 -0
- package/scripts/clean-linear-backlog.js +273 -0
- package/scripts/cleanup-old-sessions.sh +57 -0
- package/scripts/codex-wrapper.sh +88 -0
- package/scripts/create-sandbox.sh +269 -0
- package/scripts/debug-linear-update.js +174 -0
- package/scripts/delete-linear-tasks.js +167 -0
- package/scripts/deploy.sh +89 -0
- package/scripts/deployment/railway.sh +352 -0
- package/scripts/deployment/test-deployment.js +194 -0
- package/scripts/detect-and-rehydrate.js +162 -0
- package/scripts/detect-and-rehydrate.mjs +165 -0
- package/scripts/development/create-demo-tasks.js +143 -0
- package/scripts/development/debug-frame-test.js +16 -0
- package/scripts/development/demo-auto-sync.js +128 -0
- package/scripts/development/fix-all-imports.js +213 -0
- package/scripts/development/fix-imports.js +229 -0
- package/scripts/development/fix-lint-loop.cjs +103 -0
- package/scripts/development/fix-project-id.ts +161 -0
- package/scripts/development/fix-strict-mode-issues.ts +291 -0
- package/scripts/development/reorganize-structure.sh +228 -0
- package/scripts/development/test-persistence-direct.js +148 -0
- package/scripts/development/test-persistence.js +114 -0
- package/scripts/development/test-tasks.js +93 -0
- package/scripts/development/update-imports.js +212 -0
- package/scripts/fetch-linear-status.js +125 -0
- package/scripts/git-hooks/README.md +310 -0
- package/scripts/git-hooks/branch-context-manager.sh +342 -0
- package/scripts/git-hooks/post-checkout-stackmemory.sh +63 -0
- package/scripts/git-hooks/post-commit-stackmemory.sh +305 -0
- package/scripts/git-hooks/pre-commit-stackmemory.sh +275 -0
- package/scripts/hooks/cleanup-shell.sh +130 -0
- package/scripts/hooks/task-complete.sh +114 -0
- package/scripts/initialize.ts +129 -0
- package/scripts/install-claude-hooks-auto.js +104 -0
- package/scripts/install-claude-hooks.sh +133 -0
- package/scripts/install-global.sh +296 -0
- package/scripts/install.sh +235 -0
- package/scripts/linear-auto-sync.js +262 -0
- package/scripts/linear-auto-sync.sh +161 -0
- package/scripts/linear-sync-daemon.js +150 -0
- package/scripts/linear-task-review.js +237 -0
- package/scripts/list-linear-tasks.ts +178 -0
- package/scripts/mcp-proxy.js +66 -0
- package/scripts/opencode-wrapper.sh +85 -0
- package/scripts/publish-local.js +74 -0
- package/scripts/query-chromadb.ts +201 -0
- package/scripts/railway-env-setup.sh +39 -0
- package/scripts/reconcile-local-tasks.js +170 -0
- package/scripts/recreate-frames-db.js +89 -0
- package/scripts/setup/claude-integration.js +138 -0
- package/scripts/setup/configure-alias.js +125 -0
- package/scripts/setup/configure-codex-alias.js +161 -0
- package/scripts/setup/configure-opencode-alias.js +175 -0
- package/scripts/setup-claude-integration.js +204 -0
- package/scripts/setup-claude-integration.sh +183 -0
- package/scripts/setup.sh +31 -0
- package/scripts/show-linear-summary.ts +172 -0
- package/scripts/stackmemory-auto-handoff.sh +231 -0
- package/scripts/stackmemory-daemon.sh +40 -0
- package/scripts/start-linear-sync-daemon.sh +141 -0
- package/scripts/start-temporal-paradox.sh +214 -0
- package/scripts/status.ts +159 -0
- package/scripts/sync-and-clean-tasks.js +258 -0
- package/scripts/sync-frames-from-railway.js +228 -0
- package/scripts/sync-linear-graphql.js +303 -0
- package/scripts/sync-linear-tasks.js +186 -0
- package/scripts/test-auto-triggers.sh +57 -0
- package/scripts/test-browser-mcp.js +74 -0
- package/scripts/test-chromadb-full.js +115 -0
- package/scripts/test-chromadb-hooks.sh +28 -0
- package/scripts/test-chromadb-sync.ts +245 -0
- package/scripts/test-cli-security.js +293 -0
- package/scripts/test-hooks-persistence.sh +220 -0
- package/scripts/test-installation-scenarios.sh +359 -0
- package/scripts/test-installation.sh +224 -0
- package/scripts/test-mcp.js +163 -0
- package/scripts/test-pre-publish-quick.sh +75 -0
- package/scripts/test-quality-gates.sh +263 -0
- package/scripts/test-railway-db.js +222 -0
- package/scripts/test-redis-storage.ts +490 -0
- package/scripts/test-rlm-basic.sh +122 -0
- package/scripts/test-rlm-comprehensive.sh +260 -0
- package/scripts/test-rlm-e2e.sh +268 -0
- package/scripts/test-rlm-simple.js +90 -0
- package/scripts/test-rlm.js +110 -0
- package/scripts/test-session-handoff.sh +165 -0
- package/scripts/test-shell-integration.sh +275 -0
- package/scripts/testing/ab-test-runner.ts +508 -0
- package/scripts/testing/collect-metrics.ts +457 -0
- package/scripts/testing/quick-effectiveness-demo.js +187 -0
- package/scripts/testing/real-performance-test.js +422 -0
- package/scripts/testing/run-effectiveness-tests.sh +176 -0
- package/scripts/testing/scripts/testing/ab-test-runner.js +363 -0
- package/scripts/testing/scripts/testing/collect-metrics.js +292 -0
- package/scripts/testing/simple-effectiveness-test.js +310 -0
- package/scripts/testing/src/core/context/context-bridge.js +253 -0
- package/scripts/testing/src/core/context/frame-manager.js +746 -0
- package/scripts/testing/src/core/context/shared-context-layer.js +437 -0
- package/scripts/testing/src/core/database/database-adapter.js +54 -0
- package/scripts/testing/src/core/errors/index.js +291 -0
- package/scripts/testing/src/core/errors/recovery.js +268 -0
- package/scripts/testing/src/core/monitoring/logger.js +145 -0
- package/scripts/testing/src/core/retrieval/context-retriever.js +516 -0
- package/scripts/testing/src/core/session/index.js +1 -0
- package/scripts/testing/src/core/session/session-manager.js +323 -0
- package/scripts/testing/src/core/trace/cli-trace-wrapper.js +140 -0
- package/scripts/testing/src/core/trace/db-trace-wrapper.js +251 -0
- package/scripts/testing/src/core/trace/debug-trace.js +398 -0
- package/scripts/testing/src/core/trace/index.js +120 -0
- package/scripts/testing/src/core/trace/linear-api-wrapper.js +204 -0
- package/scripts/update-linear-status.js +268 -0
- package/scripts/update-linear-tasks-fixed.js +284 -0
- package/templates/claude-hooks/hooks.json +5 -0
- package/templates/claude-hooks/on-clear.js +56 -0
- package/templates/claude-hooks/on-startup.js +56 -0
- package/templates/claude-hooks/tool-use-trace.js +67 -0
- package/dist/features/tui/components/analytics-panel.js +0 -157
- package/dist/features/tui/components/analytics-panel.js.map +0 -7
- package/dist/features/tui/components/frame-visualizer.js +0 -377
- package/dist/features/tui/components/frame-visualizer.js.map +0 -7
- package/dist/features/tui/components/pr-tracker.js +0 -135
- package/dist/features/tui/components/pr-tracker.js.map +0 -7
- package/dist/features/tui/components/session-monitor.js +0 -299
- package/dist/features/tui/components/session-monitor.js.map +0 -7
- package/dist/features/tui/components/subagent-fleet.js +0 -395
- package/dist/features/tui/components/subagent-fleet.js.map +0 -7
- package/dist/features/tui/components/task-board.js +0 -1139
- package/dist/features/tui/components/task-board.js.map +0 -7
- package/dist/features/tui/index.js +0 -408
- package/dist/features/tui/index.js.map +0 -7
- package/dist/features/tui/services/data-service.js +0 -641
- package/dist/features/tui/services/data-service.js.map +0 -7
- package/dist/features/tui/services/linear-task-reader.js +0 -102
- package/dist/features/tui/services/linear-task-reader.js.map +0 -7
- package/dist/features/tui/services/websocket-client.js +0 -162
- package/dist/features/tui/services/websocket-client.js.map +0 -7
- package/dist/features/tui/terminal-compat.js +0 -220
- package/dist/features/tui/terminal-compat.js.map +0 -7
- package/dist/features/tui/types.js +0 -1
- package/dist/features/tui/types.js.map +0 -7
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Script to merge duplicate Linear tasks (with dry-run option)
|
|
5
|
+
* Keeps the lowest-numbered task as primary and marks others as duplicates
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
|
|
11
|
+
interface DuplicateGroup {
|
|
12
|
+
name: string;
|
|
13
|
+
taskIds: string[];
|
|
14
|
+
primaryId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface LinearTeam {
|
|
18
|
+
id: string;
|
|
19
|
+
key: string;
|
|
20
|
+
name: string;
|
|
21
|
+
states: {
|
|
22
|
+
nodes: WorkflowState[];
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface WorkflowState {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
type: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface LinearIssue {
|
|
33
|
+
id: string;
|
|
34
|
+
identifier: string;
|
|
35
|
+
title: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
state: {
|
|
38
|
+
name: string;
|
|
39
|
+
type: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const duplicateGroups: DuplicateGroup[] = [
|
|
44
|
+
{
|
|
45
|
+
name: 'Linear API Integration',
|
|
46
|
+
taskIds: ['STA-88', 'STA-74', 'STA-61', 'STA-46', 'STA-32', 'STA-9'],
|
|
47
|
+
primaryId: 'STA-9',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'Performance Optimization',
|
|
51
|
+
taskIds: [
|
|
52
|
+
'STA-87',
|
|
53
|
+
'STA-73',
|
|
54
|
+
'STA-60',
|
|
55
|
+
'STA-45',
|
|
56
|
+
'STA-31',
|
|
57
|
+
'STA-13',
|
|
58
|
+
'STA-21',
|
|
59
|
+
'STA-35',
|
|
60
|
+
'STA-50',
|
|
61
|
+
'STA-63',
|
|
62
|
+
'STA-77',
|
|
63
|
+
],
|
|
64
|
+
primaryId: 'STA-13',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'Security Audit and Input Validation',
|
|
68
|
+
taskIds: ['STA-85', 'STA-71', 'STA-58', 'STA-43', 'STA-29'],
|
|
69
|
+
primaryId: 'STA-29',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'Implement Proper Error Handling',
|
|
73
|
+
taskIds: ['STA-84', 'STA-70', 'STA-57', 'STA-42', 'STA-28'],
|
|
74
|
+
primaryId: 'STA-28',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'Implement Comprehensive Testing Suite',
|
|
78
|
+
taskIds: ['STA-83', 'STA-69', 'STA-56', 'STA-41', 'STA-27'],
|
|
79
|
+
primaryId: 'STA-27',
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
async function mergeDuplicateTasks(dryRun = true) {
|
|
84
|
+
const mode = dryRun ? '🔍 DRY RUN MODE' : '⚡ LIVE MODE';
|
|
85
|
+
console.log(`\n${mode} - Linear duplicate task merge\n`);
|
|
86
|
+
console.log('='.repeat(60));
|
|
87
|
+
|
|
88
|
+
// Load Linear tokens
|
|
89
|
+
const tokensPath = join(process.cwd(), '.stackmemory', 'linear-tokens.json');
|
|
90
|
+
let accessToken: string;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const tokensData = readFileSync(tokensPath, 'utf8');
|
|
94
|
+
const tokens = JSON.parse(tokensData);
|
|
95
|
+
accessToken = tokens.accessToken;
|
|
96
|
+
console.log('✅ Loaded Linear authentication tokens\n');
|
|
97
|
+
} catch {
|
|
98
|
+
console.error(
|
|
99
|
+
'❌ Failed to load Linear tokens. Please run: stackmemory linear setup'
|
|
100
|
+
);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Initialize Linear client using GraphQL directly
|
|
105
|
+
const linearApiUrl = 'https://api.linear.app/graphql';
|
|
106
|
+
|
|
107
|
+
async function graphqlRequest(
|
|
108
|
+
query: string,
|
|
109
|
+
variables: Record<string, unknown> = {}
|
|
110
|
+
) {
|
|
111
|
+
const response = await fetch(linearApiUrl, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: {
|
|
114
|
+
Authorization: `Bearer ${accessToken}`,
|
|
115
|
+
'Content-Type': 'application/json',
|
|
116
|
+
},
|
|
117
|
+
body: JSON.stringify({ query, variables }),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Linear API error: ${response.status} ${response.statusText}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const result = (await response.json()) as {
|
|
127
|
+
errors?: unknown[];
|
|
128
|
+
data: unknown;
|
|
129
|
+
};
|
|
130
|
+
if (result.errors) {
|
|
131
|
+
throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return result.data;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Get team and workflow states
|
|
138
|
+
console.log('Fetching team information...');
|
|
139
|
+
const teamQuery = `
|
|
140
|
+
query {
|
|
141
|
+
teams {
|
|
142
|
+
nodes {
|
|
143
|
+
id
|
|
144
|
+
key
|
|
145
|
+
name
|
|
146
|
+
states {
|
|
147
|
+
nodes {
|
|
148
|
+
id
|
|
149
|
+
name
|
|
150
|
+
type
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
const teamData = (await graphqlRequest(teamQuery)) as {
|
|
159
|
+
teams: { nodes: LinearTeam[] };
|
|
160
|
+
};
|
|
161
|
+
const team = teamData.teams.nodes[0];
|
|
162
|
+
|
|
163
|
+
if (!team) {
|
|
164
|
+
console.error('❌ No team found');
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const canceledState = team.states.nodes.find(
|
|
169
|
+
(s: WorkflowState) => s.type === 'canceled'
|
|
170
|
+
);
|
|
171
|
+
if (!canceledState) {
|
|
172
|
+
console.error('❌ No canceled state found in team workflow');
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(`✅ Found team: ${team.name} (${team.key})`);
|
|
177
|
+
console.log(`✅ Canceled state ID: ${canceledState.id}\n`);
|
|
178
|
+
|
|
179
|
+
// Process each duplicate group
|
|
180
|
+
let totalUpdates = 0;
|
|
181
|
+
let totalCanceled = 0;
|
|
182
|
+
|
|
183
|
+
for (const group of duplicateGroups) {
|
|
184
|
+
console.log(`\n📋 Processing: ${group.name}`);
|
|
185
|
+
console.log(` Primary: ${group.primaryId}`);
|
|
186
|
+
console.log(
|
|
187
|
+
` Will cancel: ${group.taskIds.filter((id) => id !== group.primaryId).join(', ')}`
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
if (dryRun) {
|
|
191
|
+
console.log(' 🔍 [DRY RUN] Would:');
|
|
192
|
+
console.log(` - Keep ${group.primaryId} as primary`);
|
|
193
|
+
console.log(` - Cancel ${group.taskIds.length - 1} duplicates`);
|
|
194
|
+
console.log(` - Merge any unique descriptions into primary`);
|
|
195
|
+
totalCanceled += group.taskIds.length - 1;
|
|
196
|
+
} else {
|
|
197
|
+
try {
|
|
198
|
+
// Get all issues in the group
|
|
199
|
+
const issueQuery = `
|
|
200
|
+
query GetIssue($identifier: String!) {
|
|
201
|
+
issue(id: $identifier) {
|
|
202
|
+
id
|
|
203
|
+
identifier
|
|
204
|
+
title
|
|
205
|
+
description
|
|
206
|
+
state {
|
|
207
|
+
id
|
|
208
|
+
name
|
|
209
|
+
type
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
// Get primary issue
|
|
216
|
+
const primaryData = (await graphqlRequest(issueQuery, {
|
|
217
|
+
identifier: group.primaryId,
|
|
218
|
+
})) as { issue: LinearIssue | null };
|
|
219
|
+
const primaryIssue = primaryData.issue;
|
|
220
|
+
|
|
221
|
+
if (!primaryIssue) {
|
|
222
|
+
console.log(
|
|
223
|
+
` ⚠️ Primary issue ${group.primaryId} not found, skipping group`
|
|
224
|
+
);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Process duplicates
|
|
229
|
+
const duplicateIds = group.taskIds.filter(
|
|
230
|
+
(id) => id !== group.primaryId
|
|
231
|
+
);
|
|
232
|
+
const mergedDescriptions: string[] = [];
|
|
233
|
+
|
|
234
|
+
for (const duplicateId of duplicateIds) {
|
|
235
|
+
try {
|
|
236
|
+
const dupData = (await graphqlRequest(issueQuery, {
|
|
237
|
+
identifier: duplicateId,
|
|
238
|
+
})) as { issue: LinearIssue | null };
|
|
239
|
+
const duplicateIssue = dupData.issue;
|
|
240
|
+
|
|
241
|
+
if (!duplicateIssue) {
|
|
242
|
+
console.log(` ⚠️ Issue ${duplicateId} not found, skipping`);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Collect unique descriptions
|
|
247
|
+
if (
|
|
248
|
+
duplicateIssue.description &&
|
|
249
|
+
duplicateIssue.description !== primaryIssue.description
|
|
250
|
+
) {
|
|
251
|
+
mergedDescriptions.push(
|
|
252
|
+
`[Merged from ${duplicateId}]\n${duplicateIssue.description}`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Cancel the duplicate
|
|
257
|
+
const updateMutation = `
|
|
258
|
+
mutation UpdateIssue($id: String!, $stateId: String!, $description: String!) {
|
|
259
|
+
issueUpdate(
|
|
260
|
+
id: $id,
|
|
261
|
+
input: {
|
|
262
|
+
stateId: $stateId,
|
|
263
|
+
description: $description
|
|
264
|
+
}
|
|
265
|
+
) {
|
|
266
|
+
success
|
|
267
|
+
issue {
|
|
268
|
+
identifier
|
|
269
|
+
state {
|
|
270
|
+
name
|
|
271
|
+
type
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
`;
|
|
277
|
+
|
|
278
|
+
await graphqlRequest(updateMutation, {
|
|
279
|
+
id: duplicateIssue.id,
|
|
280
|
+
stateId: canceledState.id,
|
|
281
|
+
description: `Duplicate of ${group.primaryId}\n\n${duplicateIssue.description || ''}`,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
console.log(` ✅ Canceled ${duplicateId} as duplicate`);
|
|
285
|
+
totalCanceled++;
|
|
286
|
+
} catch (error: unknown) {
|
|
287
|
+
const errorMessage =
|
|
288
|
+
error instanceof Error ? error.message : String(error);
|
|
289
|
+
console.log(
|
|
290
|
+
` ❌ Failed to process ${duplicateId}: ${errorMessage}`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Update primary with merged descriptions if any
|
|
296
|
+
if (mergedDescriptions.length > 0) {
|
|
297
|
+
const newDescription = [
|
|
298
|
+
primaryIssue.description || '',
|
|
299
|
+
'',
|
|
300
|
+
'--- Merged Content ---',
|
|
301
|
+
...mergedDescriptions,
|
|
302
|
+
]
|
|
303
|
+
.filter(Boolean)
|
|
304
|
+
.join('\n\n');
|
|
305
|
+
|
|
306
|
+
const updatePrimaryMutation = `
|
|
307
|
+
mutation UpdatePrimaryIssue($id: String!, $description: String!) {
|
|
308
|
+
issueUpdate(
|
|
309
|
+
id: $id,
|
|
310
|
+
input: {
|
|
311
|
+
description: $description
|
|
312
|
+
}
|
|
313
|
+
) {
|
|
314
|
+
success
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
`;
|
|
318
|
+
|
|
319
|
+
await graphqlRequest(updatePrimaryMutation, {
|
|
320
|
+
id: primaryIssue.id,
|
|
321
|
+
description: newDescription,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
console.log(
|
|
325
|
+
` ✅ Updated ${group.primaryId} with merged descriptions`
|
|
326
|
+
);
|
|
327
|
+
totalUpdates++;
|
|
328
|
+
}
|
|
329
|
+
} catch (error: unknown) {
|
|
330
|
+
const errorMessage =
|
|
331
|
+
error instanceof Error ? error.message : String(error);
|
|
332
|
+
console.error(` ❌ Error processing group: ${errorMessage}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Summary
|
|
338
|
+
console.log('\n' + '='.repeat(60));
|
|
339
|
+
console.log(`\n✨ ${dryRun ? 'DRY RUN' : 'MERGE'} COMPLETE!\n`);
|
|
340
|
+
console.log('📊 Summary:');
|
|
341
|
+
console.log(` Groups processed: ${duplicateGroups.length}`);
|
|
342
|
+
console.log(` Primary tasks kept: ${duplicateGroups.length}`);
|
|
343
|
+
console.log(
|
|
344
|
+
` Tasks ${dryRun ? 'to cancel' : 'canceled'}: ${dryRun ? duplicateGroups.reduce((acc, g) => acc + g.taskIds.length - 1, 0) : totalCanceled}`
|
|
345
|
+
);
|
|
346
|
+
if (!dryRun) {
|
|
347
|
+
console.log(` Primary tasks updated: ${totalUpdates}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (dryRun) {
|
|
351
|
+
console.log('\n💡 To execute these changes, run with --execute flag');
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Parse command line arguments
|
|
356
|
+
const isDryRun = !process.argv.includes('--execute');
|
|
357
|
+
|
|
358
|
+
// Run the merge
|
|
359
|
+
mergeDuplicateTasks(isDryRun).catch((error) => {
|
|
360
|
+
console.error('❌ Fatal error:', error);
|
|
361
|
+
process.exit(1);
|
|
362
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Script to merge duplicate Linear tasks
|
|
5
|
+
* Keeps the lowest-numbered task as primary and marks others as duplicates
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { LinearClient } from '@linear/sdk';
|
|
9
|
+
import { readFileSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
|
|
12
|
+
interface DuplicateGroup {
|
|
13
|
+
name: string;
|
|
14
|
+
taskIds: string[];
|
|
15
|
+
primaryId: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const duplicateGroups: DuplicateGroup[] = [
|
|
19
|
+
{
|
|
20
|
+
name: 'Linear API Integration',
|
|
21
|
+
taskIds: ['STA-88', 'STA-74', 'STA-61', 'STA-46', 'STA-32', 'STA-9'],
|
|
22
|
+
primaryId: 'STA-9',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'Performance Optimization',
|
|
26
|
+
taskIds: [
|
|
27
|
+
'STA-87',
|
|
28
|
+
'STA-73',
|
|
29
|
+
'STA-60',
|
|
30
|
+
'STA-45',
|
|
31
|
+
'STA-31',
|
|
32
|
+
'STA-13',
|
|
33
|
+
'STA-21',
|
|
34
|
+
'STA-35',
|
|
35
|
+
'STA-50',
|
|
36
|
+
'STA-63',
|
|
37
|
+
'STA-77',
|
|
38
|
+
],
|
|
39
|
+
primaryId: 'STA-13',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'Security Audit and Input Validation',
|
|
43
|
+
taskIds: ['STA-85', 'STA-71', 'STA-58', 'STA-43', 'STA-29'],
|
|
44
|
+
primaryId: 'STA-29',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'Implement Proper Error Handling',
|
|
48
|
+
taskIds: ['STA-84', 'STA-70', 'STA-57', 'STA-42', 'STA-28'],
|
|
49
|
+
primaryId: 'STA-28',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'Implement Comprehensive Testing Suite',
|
|
53
|
+
taskIds: ['STA-83', 'STA-69', 'STA-56', 'STA-41', 'STA-27'],
|
|
54
|
+
primaryId: 'STA-27',
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
async function mergeDuplicateTasks() {
|
|
59
|
+
console.log('🔄 Starting Linear duplicate task merge...\n');
|
|
60
|
+
|
|
61
|
+
// Load Linear tokens
|
|
62
|
+
const tokensPath = join(process.cwd(), '.stackmemory', 'linear-tokens.json');
|
|
63
|
+
let accessToken: string;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const tokensData = readFileSync(tokensPath, 'utf8');
|
|
67
|
+
const tokens = JSON.parse(tokensData);
|
|
68
|
+
accessToken = tokens.accessToken;
|
|
69
|
+
console.log('Loaded Linear authentication tokens\n');
|
|
70
|
+
} catch {
|
|
71
|
+
console.error(
|
|
72
|
+
'Failed to load Linear tokens. Please run: stackmemory linear setup'
|
|
73
|
+
);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Initialize Linear client
|
|
78
|
+
const client = new LinearClient({
|
|
79
|
+
apiKey: accessToken,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Process each duplicate group
|
|
83
|
+
for (const group of duplicateGroups) {
|
|
84
|
+
console.log(`\n📋 Processing: ${group.name}`);
|
|
85
|
+
console.log(` Primary: ${group.primaryId}`);
|
|
86
|
+
console.log(
|
|
87
|
+
` Duplicates: ${group.taskIds.filter((id) => id !== group.primaryId).join(', ')}`
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
// Get the primary issue
|
|
92
|
+
const primaryIssue = await client.issue(group.primaryId);
|
|
93
|
+
if (!primaryIssue) {
|
|
94
|
+
console.log(
|
|
95
|
+
` ⚠️ Primary issue ${group.primaryId} not found, skipping group`
|
|
96
|
+
);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Collect descriptions from all duplicates
|
|
101
|
+
let combinedDescription = primaryIssue.description || '';
|
|
102
|
+
const duplicateIds = group.taskIds.filter((id) => id !== group.primaryId);
|
|
103
|
+
|
|
104
|
+
for (const duplicateId of duplicateIds) {
|
|
105
|
+
try {
|
|
106
|
+
const duplicateIssue = await client.issue(duplicateId);
|
|
107
|
+
if (!duplicateIssue) {
|
|
108
|
+
console.log(` ⚠️ Issue ${duplicateId} not found, skipping`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Add duplicate's description if it exists and differs
|
|
113
|
+
if (
|
|
114
|
+
duplicateIssue.description &&
|
|
115
|
+
duplicateIssue.description !== primaryIssue.description
|
|
116
|
+
) {
|
|
117
|
+
combinedDescription += `\n\n---\n[Merged from ${duplicateId}]\n${duplicateIssue.description}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Get the canceled state for the team
|
|
121
|
+
const team = await duplicateIssue.team;
|
|
122
|
+
const states = await team.states();
|
|
123
|
+
const canceledState = states.nodes.find((s) => s.type === 'canceled');
|
|
124
|
+
|
|
125
|
+
if (!canceledState) {
|
|
126
|
+
console.log(
|
|
127
|
+
` ⚠️ No 'canceled' state found for team, skipping ${duplicateId}`
|
|
128
|
+
);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Update duplicate to canceled state with reference to primary
|
|
133
|
+
await duplicateIssue.update({
|
|
134
|
+
stateId: canceledState.id,
|
|
135
|
+
description: `Duplicate of ${group.primaryId}\n\n${duplicateIssue.description || ''}`,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
console.log(
|
|
139
|
+
` ✅ Marked ${duplicateId} as duplicate of ${group.primaryId}`
|
|
140
|
+
);
|
|
141
|
+
} catch (error: any) {
|
|
142
|
+
console.log(
|
|
143
|
+
` ❌ Failed to process ${duplicateId}: ${error.message}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Update primary issue with combined description if changed
|
|
149
|
+
if (combinedDescription !== primaryIssue.description) {
|
|
150
|
+
await primaryIssue.update({
|
|
151
|
+
description: combinedDescription,
|
|
152
|
+
});
|
|
153
|
+
console.log(
|
|
154
|
+
` ✅ Updated ${group.primaryId} with merged descriptions`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log(` ✅ Group "${group.name}" processed successfully`);
|
|
159
|
+
} catch (error: any) {
|
|
160
|
+
console.error(` ❌ Error processing group: ${error.message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.log('\n✨ Duplicate merge complete!');
|
|
165
|
+
console.log('\n📊 Summary:');
|
|
166
|
+
console.log(` Groups processed: ${duplicateGroups.length}`);
|
|
167
|
+
console.log(
|
|
168
|
+
` Total tasks affected: ${duplicateGroups.reduce((acc, g) => acc + g.taskIds.length, 0)}`
|
|
169
|
+
);
|
|
170
|
+
console.log(` Primary tasks kept: ${duplicateGroups.length}`);
|
|
171
|
+
console.log(
|
|
172
|
+
` Tasks marked as duplicate: ${duplicateGroups.reduce((acc, g) => acc + g.taskIds.length - 1, 0)}`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Run the merge
|
|
177
|
+
mergeDuplicateTasks().catch((error) => {
|
|
178
|
+
console.error('❌ Fatal error:', error);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
function removeSTATasks() {
|
|
10
|
+
const tasksFile = path.join(__dirname, '..', '.stackmemory', 'tasks.jsonl');
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(tasksFile)) {
|
|
13
|
+
console.error('❌ Tasks file not found:', tasksFile);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Create backup first
|
|
18
|
+
const backupFile = tasksFile + '.backup-' + Date.now();
|
|
19
|
+
fs.copyFileSync(tasksFile, backupFile);
|
|
20
|
+
console.log(`💾 Backup created: ${backupFile}`);
|
|
21
|
+
|
|
22
|
+
// Read all tasks
|
|
23
|
+
const lines = fs.readFileSync(tasksFile, 'utf8').split('\n').filter(l => l.trim());
|
|
24
|
+
|
|
25
|
+
const keptTasks = [];
|
|
26
|
+
const removedTasks = [];
|
|
27
|
+
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
try {
|
|
30
|
+
const task = JSON.parse(line);
|
|
31
|
+
|
|
32
|
+
// Check if task has STA- in title
|
|
33
|
+
const hasSTAId = task.title?.includes('[STA-');
|
|
34
|
+
|
|
35
|
+
if (hasSTAId) {
|
|
36
|
+
removedTasks.push(task);
|
|
37
|
+
} else {
|
|
38
|
+
keptTasks.push(line);
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
// Keep unparseable lines as-is
|
|
42
|
+
keptTasks.push(line);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(`\n📊 Task Analysis:`);
|
|
47
|
+
console.log(` Total tasks: ${lines.length}`);
|
|
48
|
+
console.log(` STA tasks to remove: ${removedTasks.length}`);
|
|
49
|
+
console.log(` Tasks to keep: ${keptTasks.length}`);
|
|
50
|
+
|
|
51
|
+
if (removedTasks.length > 0) {
|
|
52
|
+
console.log('\n🗑️ Removing STA tasks:');
|
|
53
|
+
for (const task of removedTasks.slice(0, 10)) {
|
|
54
|
+
const match = task.title.match(/\[(STA-\d+)\]/);
|
|
55
|
+
console.log(` - ${match?.[1] || 'STA-???'}: ${task.title.substring(0, 60)}...`);
|
|
56
|
+
}
|
|
57
|
+
if (removedTasks.length > 10) {
|
|
58
|
+
console.log(` ... and ${removedTasks.length - 10} more`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Write cleaned file
|
|
62
|
+
fs.writeFileSync(tasksFile, keptTasks.join('\n') + '\n');
|
|
63
|
+
console.log(`\n✅ Removed ${removedTasks.length} STA tasks`);
|
|
64
|
+
console.log(`📂 ${keptTasks.length} tasks remaining`);
|
|
65
|
+
} else {
|
|
66
|
+
console.log('\n✅ No STA tasks found to remove');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
removeSTATasks();
|