@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.
- package/README.md +48 -2
- 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,419 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Comprehensive Duplicate Check Script
|
|
5
|
+
* Scans all tasks in memory and checks for duplicates in Linear
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { LinearClient } from '../dist/integrations/linear/client.js';
|
|
9
|
+
import { LinearDuplicateDetector } from '../dist/integrations/linear/sync-enhanced.js';
|
|
10
|
+
import { LinearAuthManager } from '../dist/integrations/linear/auth.js';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { existsSync, writeFileSync, readFileSync } from 'fs';
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import ora from 'ora';
|
|
15
|
+
import Table from 'cli-table3';
|
|
16
|
+
|
|
17
|
+
interface DuplicateReport {
|
|
18
|
+
taskId: string;
|
|
19
|
+
taskTitle: string;
|
|
20
|
+
duplicates: Array<{
|
|
21
|
+
linearId: string;
|
|
22
|
+
identifier: string;
|
|
23
|
+
title: string;
|
|
24
|
+
similarity: number;
|
|
25
|
+
url: string;
|
|
26
|
+
}>;
|
|
27
|
+
recommendation: 'merge' | 'skip' | 'review';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class DuplicateChecker {
|
|
31
|
+
private linearClient: LinearClient;
|
|
32
|
+
private duplicateDetector: LinearDuplicateDetector;
|
|
33
|
+
private authManager: LinearAuthManager;
|
|
34
|
+
private projectRoot: string;
|
|
35
|
+
private report: DuplicateReport[] = [];
|
|
36
|
+
|
|
37
|
+
constructor() {
|
|
38
|
+
this.projectRoot = process.cwd();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async initialize(): Promise<void> {
|
|
42
|
+
// Check database
|
|
43
|
+
const dbPath = join(this.projectRoot, '.stackmemory', 'context.db');
|
|
44
|
+
if (!existsSync(dbPath)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
'StackMemory not initialized. Run "stackmemory init" first.'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Initialize Linear auth - check env var first
|
|
51
|
+
const envApiKey = process.env.LINEAR_API_KEY;
|
|
52
|
+
|
|
53
|
+
if (envApiKey) {
|
|
54
|
+
// Use environment variable API key
|
|
55
|
+
this.linearClient = new LinearClient({
|
|
56
|
+
apiKey: envApiKey,
|
|
57
|
+
useBearer: false,
|
|
58
|
+
});
|
|
59
|
+
} else {
|
|
60
|
+
// Try OAuth auth
|
|
61
|
+
this.authManager = new LinearAuthManager(this.projectRoot);
|
|
62
|
+
const token = await this.authManager.getValidToken();
|
|
63
|
+
|
|
64
|
+
if (!token) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
'Linear not authenticated. Set LINEAR_API_KEY env var or run "stackmemory linear auth".'
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check if using OAuth by looking for refresh token
|
|
71
|
+
const tokens = this.authManager.loadTokens();
|
|
72
|
+
const isOAuth = !!(tokens && tokens.refreshToken);
|
|
73
|
+
|
|
74
|
+
this.linearClient = new LinearClient({
|
|
75
|
+
apiKey: token,
|
|
76
|
+
useBearer: isOAuth,
|
|
77
|
+
onUnauthorized: isOAuth
|
|
78
|
+
? async () => {
|
|
79
|
+
const refreshed = await this.authManager.refreshAccessToken();
|
|
80
|
+
return refreshed.accessToken;
|
|
81
|
+
}
|
|
82
|
+
: undefined,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Initialize duplicate detector
|
|
87
|
+
this.duplicateDetector = new LinearDuplicateDetector(this.linearClient);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async runFullScan(): Promise<void> {
|
|
91
|
+
console.log(chalk.cyan('\nš Starting Comprehensive Duplicate Check\n'));
|
|
92
|
+
|
|
93
|
+
// Get all tasks from memory
|
|
94
|
+
const spinner = ora('Loading tasks from memory...').start();
|
|
95
|
+
|
|
96
|
+
// Read tasks from JSONL file
|
|
97
|
+
const tasksFile = join(this.projectRoot, '.stackmemory', 'tasks.jsonl');
|
|
98
|
+
const tasksData = existsSync(tasksFile)
|
|
99
|
+
? readFileSync(tasksFile, 'utf8')
|
|
100
|
+
.split('\n')
|
|
101
|
+
.filter((line) => line.trim())
|
|
102
|
+
: [];
|
|
103
|
+
const tasks = tasksData.map((line) => JSON.parse(line));
|
|
104
|
+
|
|
105
|
+
spinner.succeed(`Loaded ${tasks.length} tasks from memory`);
|
|
106
|
+
|
|
107
|
+
// Get default team ID
|
|
108
|
+
spinner.start('Connecting to Linear...');
|
|
109
|
+
const teams = await this.linearClient.getTeams();
|
|
110
|
+
const defaultTeamId = teams[0]?.id;
|
|
111
|
+
spinner.succeed(`Connected to Linear (Team: ${teams[0]?.name})`);
|
|
112
|
+
|
|
113
|
+
// Progress tracking
|
|
114
|
+
let checked = 0;
|
|
115
|
+
let duplicatesFound = 0;
|
|
116
|
+
const startTime = Date.now();
|
|
117
|
+
|
|
118
|
+
console.log(
|
|
119
|
+
chalk.yellow(`\nš Checking ${tasks.length} tasks for duplicates...\n`)
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Check each task for duplicates
|
|
123
|
+
for (const task of tasks) {
|
|
124
|
+
checked++;
|
|
125
|
+
|
|
126
|
+
// Update progress
|
|
127
|
+
const progress = Math.round((checked / tasks.length) * 100);
|
|
128
|
+
spinner.start(
|
|
129
|
+
`[${progress}%] Checking: ${task.title.substring(0, 50)}...`
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
// Skip if task already has Linear ID mapped
|
|
134
|
+
if (task.external_refs?.linear_id) {
|
|
135
|
+
spinner.info(
|
|
136
|
+
`[${progress}%] Skipped (already mapped): ${task.title.substring(0, 40)}...`
|
|
137
|
+
);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check for duplicates
|
|
142
|
+
const duplicateCheck = await this.duplicateDetector.checkForDuplicate(
|
|
143
|
+
task.title,
|
|
144
|
+
defaultTeamId
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
if (duplicateCheck.isDuplicate && duplicateCheck.existingIssue) {
|
|
148
|
+
duplicatesFound++;
|
|
149
|
+
|
|
150
|
+
// Add to report
|
|
151
|
+
this.report.push({
|
|
152
|
+
taskId: task.id,
|
|
153
|
+
taskTitle: task.title,
|
|
154
|
+
duplicates: [
|
|
155
|
+
{
|
|
156
|
+
linearId: duplicateCheck.existingIssue.id,
|
|
157
|
+
identifier: duplicateCheck.existingIssue.identifier,
|
|
158
|
+
title: duplicateCheck.existingIssue.title,
|
|
159
|
+
similarity: duplicateCheck.similarity || 0,
|
|
160
|
+
url: duplicateCheck.existingIssue.url,
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
recommendation:
|
|
164
|
+
duplicateCheck.similarity! > 0.95
|
|
165
|
+
? 'merge'
|
|
166
|
+
: duplicateCheck.similarity! > 0.85
|
|
167
|
+
? 'review'
|
|
168
|
+
: 'skip',
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
spinner.warn(
|
|
172
|
+
`[${progress}%] DUPLICATE FOUND: "${task.title.substring(0, 30)}..." ā ${duplicateCheck.existingIssue.identifier} (${Math.round((duplicateCheck.similarity || 0) * 100)}% match)`
|
|
173
|
+
);
|
|
174
|
+
} else {
|
|
175
|
+
spinner.succeed(
|
|
176
|
+
`[${progress}%] No duplicates: ${task.title.substring(0, 40)}...`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Rate limiting delay
|
|
181
|
+
await this.delay(100);
|
|
182
|
+
} catch (error: unknown) {
|
|
183
|
+
spinner.fail(
|
|
184
|
+
`[${progress}%] Error checking task: ${(error as Error).message}`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const duration = Math.round((Date.now() - startTime) / 1000);
|
|
190
|
+
|
|
191
|
+
console.log(chalk.green(`\nā
Duplicate check completed in ${duration}s`));
|
|
192
|
+
console.log(chalk.cyan(`\nš Summary:`));
|
|
193
|
+
console.log(` ⢠Total tasks checked: ${checked}`);
|
|
194
|
+
console.log(` ⢠Duplicates found: ${duplicatesFound}`);
|
|
195
|
+
console.log(
|
|
196
|
+
` ⢠Check rate: ${Math.round(checked / duration)} tasks/second`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
displayReport(): void {
|
|
201
|
+
if (this.report.length === 0) {
|
|
202
|
+
console.log(
|
|
203
|
+
chalk.green('\n⨠No duplicates found! Your tasks are unique.\n')
|
|
204
|
+
);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log(
|
|
209
|
+
chalk.yellow(`\nā ļø Found ${this.report.length} potential duplicates:\n`)
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// Group by recommendation
|
|
213
|
+
const mergeItems = this.report.filter((r) => r.recommendation === 'merge');
|
|
214
|
+
const reviewItems = this.report.filter(
|
|
215
|
+
(r) => r.recommendation === 'review'
|
|
216
|
+
);
|
|
217
|
+
const skipItems = this.report.filter((r) => r.recommendation === 'skip');
|
|
218
|
+
|
|
219
|
+
// Display high confidence duplicates (merge)
|
|
220
|
+
if (mergeItems.length > 0) {
|
|
221
|
+
console.log(
|
|
222
|
+
chalk.red(
|
|
223
|
+
'š“ High Confidence Duplicates (>95% match) - Recommend Merge:\n'
|
|
224
|
+
)
|
|
225
|
+
);
|
|
226
|
+
const table = new Table({
|
|
227
|
+
head: ['Local Task', 'Linear Issue', 'Match %', 'URL'],
|
|
228
|
+
style: { head: ['red'] },
|
|
229
|
+
colWidths: [40, 15, 10, 50],
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
mergeItems.forEach((item) => {
|
|
233
|
+
item.duplicates.forEach((dup) => {
|
|
234
|
+
table.push([
|
|
235
|
+
item.taskTitle.substring(0, 38),
|
|
236
|
+
dup.identifier,
|
|
237
|
+
`${Math.round(dup.similarity * 100)}%`,
|
|
238
|
+
dup.url.substring(0, 48),
|
|
239
|
+
]);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
console.log(table.toString());
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Display medium confidence duplicates (review)
|
|
247
|
+
if (reviewItems.length > 0) {
|
|
248
|
+
console.log(
|
|
249
|
+
chalk.yellow(
|
|
250
|
+
'\nš” Medium Confidence Duplicates (85-95% match) - Recommend Review:\n'
|
|
251
|
+
)
|
|
252
|
+
);
|
|
253
|
+
const table = new Table({
|
|
254
|
+
head: ['Local Task', 'Linear Issue', 'Match %', 'Linear Title'],
|
|
255
|
+
style: { head: ['yellow'] },
|
|
256
|
+
colWidths: [35, 15, 10, 40],
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
reviewItems.forEach((item) => {
|
|
260
|
+
item.duplicates.forEach((dup) => {
|
|
261
|
+
table.push([
|
|
262
|
+
item.taskTitle.substring(0, 33),
|
|
263
|
+
dup.identifier,
|
|
264
|
+
`${Math.round(dup.similarity * 100)}%`,
|
|
265
|
+
dup.title.substring(0, 38),
|
|
266
|
+
]);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
console.log(table.toString());
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Display low confidence (skip)
|
|
274
|
+
if (skipItems.length > 0) {
|
|
275
|
+
console.log(
|
|
276
|
+
chalk.gray(
|
|
277
|
+
`\nšµ Low Confidence Matches (<85%) - ${skipItems.length} items (not shown)\n`
|
|
278
|
+
)
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async saveReport(): Promise<void> {
|
|
284
|
+
if (this.report.length === 0) return;
|
|
285
|
+
|
|
286
|
+
const reportPath = join(
|
|
287
|
+
this.projectRoot,
|
|
288
|
+
'.stackmemory',
|
|
289
|
+
'duplicate-report.json'
|
|
290
|
+
);
|
|
291
|
+
const markdownPath = join(
|
|
292
|
+
this.projectRoot,
|
|
293
|
+
'.stackmemory',
|
|
294
|
+
'duplicate-report.md'
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
// Save JSON report
|
|
298
|
+
writeFileSync(reportPath, JSON.stringify(this.report, null, 2));
|
|
299
|
+
|
|
300
|
+
// Generate markdown report
|
|
301
|
+
let markdown = '# Linear Duplicate Check Report\n\n';
|
|
302
|
+
markdown += `**Generated:** ${new Date().toLocaleString()}\n`;
|
|
303
|
+
markdown += `**Total Duplicates Found:** ${this.report.length}\n\n`;
|
|
304
|
+
|
|
305
|
+
// High confidence section
|
|
306
|
+
const mergeItems = this.report.filter((r) => r.recommendation === 'merge');
|
|
307
|
+
if (mergeItems.length > 0) {
|
|
308
|
+
markdown += '## š“ High Confidence Duplicates (>95% match)\n\n';
|
|
309
|
+
markdown += 'These should be merged:\n\n';
|
|
310
|
+
mergeItems.forEach((item) => {
|
|
311
|
+
item.duplicates.forEach((dup) => {
|
|
312
|
+
markdown += `- **${item.taskTitle}**\n`;
|
|
313
|
+
markdown += ` - Linear: [${dup.identifier}](${dup.url}) - ${Math.round(dup.similarity * 100)}% match\n`;
|
|
314
|
+
markdown += ` - Action: MERGE\n\n`;
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Medium confidence section
|
|
320
|
+
const reviewItems = this.report.filter(
|
|
321
|
+
(r) => r.recommendation === 'review'
|
|
322
|
+
);
|
|
323
|
+
if (reviewItems.length > 0) {
|
|
324
|
+
markdown += '## š” Medium Confidence Duplicates (85-95% match)\n\n';
|
|
325
|
+
markdown += 'These need manual review:\n\n';
|
|
326
|
+
reviewItems.forEach((item) => {
|
|
327
|
+
item.duplicates.forEach((dup) => {
|
|
328
|
+
markdown += `- **${item.taskTitle}**\n`;
|
|
329
|
+
markdown += ` - Linear: [${dup.identifier}](${dup.url}) - "${dup.title}"\n`;
|
|
330
|
+
markdown += ` - Match: ${Math.round(dup.similarity * 100)}%\n`;
|
|
331
|
+
markdown += ` - Action: REVIEW\n\n`;
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Save markdown
|
|
337
|
+
writeFileSync(markdownPath, markdown);
|
|
338
|
+
|
|
339
|
+
console.log(chalk.green(`\nš Reports saved:`));
|
|
340
|
+
console.log(chalk.gray(` ⢠JSON: ${reportPath}`));
|
|
341
|
+
console.log(chalk.gray(` ⢠Markdown: ${markdownPath}`));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async suggestActions(): Promise<void> {
|
|
345
|
+
const mergeCount = this.report.filter(
|
|
346
|
+
(r) => r.recommendation === 'merge'
|
|
347
|
+
).length;
|
|
348
|
+
const reviewCount = this.report.filter(
|
|
349
|
+
(r) => r.recommendation === 'review'
|
|
350
|
+
).length;
|
|
351
|
+
|
|
352
|
+
if (mergeCount > 0 || reviewCount > 0) {
|
|
353
|
+
console.log(chalk.cyan('\nš” Recommended Actions:\n'));
|
|
354
|
+
|
|
355
|
+
if (mergeCount > 0) {
|
|
356
|
+
console.log(chalk.green('1. Auto-merge high confidence duplicates:'));
|
|
357
|
+
console.log(
|
|
358
|
+
chalk.gray(
|
|
359
|
+
' stackmemory linear sync --merge-strategy merge_content\n'
|
|
360
|
+
)
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (reviewCount > 0) {
|
|
365
|
+
console.log(
|
|
366
|
+
chalk.yellow('2. Review medium confidence duplicates manually:')
|
|
367
|
+
);
|
|
368
|
+
console.log(
|
|
369
|
+
chalk.gray(
|
|
370
|
+
' Review the duplicate-report.md file and decide per case\n'
|
|
371
|
+
)
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
console.log(
|
|
376
|
+
chalk.blue('3. Enable duplicate prevention for future syncs:')
|
|
377
|
+
);
|
|
378
|
+
console.log(
|
|
379
|
+
chalk.gray(
|
|
380
|
+
' stackmemory linear sync --daemon --merge-strategy merge_content\n'
|
|
381
|
+
)
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private delay(ms: number): Promise<void> {
|
|
387
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Main execution
|
|
392
|
+
async function main() {
|
|
393
|
+
const checker = new DuplicateChecker();
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
// Initialize
|
|
397
|
+
await checker.initialize();
|
|
398
|
+
|
|
399
|
+
// Run full scan
|
|
400
|
+
await checker.runFullScan();
|
|
401
|
+
|
|
402
|
+
// Display report
|
|
403
|
+
checker.displayReport();
|
|
404
|
+
|
|
405
|
+
// Save reports
|
|
406
|
+
await checker.saveReport();
|
|
407
|
+
|
|
408
|
+
// Suggest actions
|
|
409
|
+
await checker.suggestActions();
|
|
410
|
+
|
|
411
|
+
process.exit(0);
|
|
412
|
+
} catch (error: unknown) {
|
|
413
|
+
console.error(chalk.red('\nā Error:'), (error as Error).message);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Run if executed directly
|
|
419
|
+
main();
|
|
@@ -0,0 +1,114 @@
|
|
|
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 cleanDuplicateTasks() {
|
|
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
|
+
const lines = fs.readFileSync(tasksFile, 'utf8').split('\n').filter(l => l.trim());
|
|
18
|
+
|
|
19
|
+
const taskMap = new Map();
|
|
20
|
+
const seenTitles = new Map();
|
|
21
|
+
const duplicates = [];
|
|
22
|
+
|
|
23
|
+
// Process each line
|
|
24
|
+
for (const line of lines) {
|
|
25
|
+
try {
|
|
26
|
+
const task = JSON.parse(line);
|
|
27
|
+
|
|
28
|
+
// Create a unique key based on title and external refs
|
|
29
|
+
let key = task.title;
|
|
30
|
+
if (task.external_refs) {
|
|
31
|
+
const linearId = Object.keys(task.external_refs).find(k => k.startsWith('STA-') || k.startsWith('ENG-'));
|
|
32
|
+
if (linearId) key = linearId;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Normalize title for comparison (remove task IDs from title)
|
|
36
|
+
const normalizedTitle = task.title
|
|
37
|
+
.replace(/^\[[^\]]+\]\s*/, '') // Remove [STA-XXX] or [ENG-XXX] prefix
|
|
38
|
+
.replace(/^\[.*?\]\s*/, '') // Remove priority markers
|
|
39
|
+
.trim();
|
|
40
|
+
|
|
41
|
+
// Track duplicates by normalized title
|
|
42
|
+
if (!seenTitles.has(normalizedTitle)) {
|
|
43
|
+
seenTitles.set(normalizedTitle, task.id);
|
|
44
|
+
taskMap.set(task.id, task);
|
|
45
|
+
} else {
|
|
46
|
+
duplicates.push({
|
|
47
|
+
id: task.id,
|
|
48
|
+
title: task.title,
|
|
49
|
+
keepId: seenTitles.get(normalizedTitle)
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.warn('Failed to parse line:', e.message);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(`\nš Task Analysis:`);
|
|
58
|
+
console.log(` Total lines: ${lines.length}`);
|
|
59
|
+
console.log(` Unique tasks: ${taskMap.size}`);
|
|
60
|
+
console.log(` Duplicates found: ${duplicates.length}`);
|
|
61
|
+
|
|
62
|
+
if (duplicates.length > 0) {
|
|
63
|
+
console.log('\nš Duplicates to remove:');
|
|
64
|
+
duplicates.forEach(d => {
|
|
65
|
+
console.log(` - ${d.id}: ${d.title}`);
|
|
66
|
+
console.log(` (keeping ${d.keepId})`);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Create backup
|
|
70
|
+
const backupFile = tasksFile + '.backup-' + Date.now();
|
|
71
|
+
fs.copyFileSync(tasksFile, backupFile);
|
|
72
|
+
console.log(`\nš¾ Backup created: ${backupFile}`);
|
|
73
|
+
|
|
74
|
+
// Write cleaned tasks
|
|
75
|
+
const cleanedLines = [];
|
|
76
|
+
for (const line of lines) {
|
|
77
|
+
try {
|
|
78
|
+
const task = JSON.parse(line);
|
|
79
|
+
const isDuplicate = duplicates.find(d => d.id === task.id);
|
|
80
|
+
if (!isDuplicate) {
|
|
81
|
+
cleanedLines.push(line);
|
|
82
|
+
}
|
|
83
|
+
} catch (e) {
|
|
84
|
+
// Keep unparseable lines as-is
|
|
85
|
+
cleanedLines.push(line);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fs.writeFileSync(tasksFile, cleanedLines.join('\n') + '\n');
|
|
90
|
+
console.log(`\nā
Cleaned tasks file written`);
|
|
91
|
+
console.log(` Removed ${duplicates.length} duplicate tasks`);
|
|
92
|
+
console.log(` Final task count: ${cleanedLines.length}`);
|
|
93
|
+
} else {
|
|
94
|
+
console.log('\nā
No duplicates found!');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Show remaining unique tasks
|
|
98
|
+
console.log('\nš Unique tasks remaining:');
|
|
99
|
+
const uniqueTasks = Array.from(taskMap.values())
|
|
100
|
+
.filter(t => t.type === 'task_create' || t.type === 'task_update')
|
|
101
|
+
.sort((a, b) => (a.priority === 'urgent' ? -1 : a.priority === 'high' ? 0 : 1));
|
|
102
|
+
|
|
103
|
+
uniqueTasks.slice(0, 10).forEach(task => {
|
|
104
|
+
const status = task.status === 'completed' ? 'ā
' :
|
|
105
|
+
task.status === 'in_progress' ? 'š' : 'ā³';
|
|
106
|
+
console.log(` ${status} [${task.priority}] ${task.title}`);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (uniqueTasks.length > 10) {
|
|
110
|
+
console.log(` ... and ${uniqueTasks.length - 10} more tasks`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
cleanDuplicateTasks();
|