clew-code 0.2.28 → 0.2.30
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 +38 -28
- package/bin/clew.cjs +2 -2
- package/dist/main.js +2318 -2331
- package/docs/architecture.html +90 -91
- package/docs/changelog.html +141 -150
- package/docs/cli-reference.html +89 -90
- package/docs/commands.html +10 -9
- package/docs/daemon.html +62 -62
- package/docs/generated/providers.html +625 -0
- package/docs/generated/tools.html +559 -0
- package/docs/index.html +10 -13
- package/docs/personal-profile.html +3 -3
- package/docs/providers.html +625 -87
- package/docs/quick-start.html +81 -81
- package/docs/tools.html +551 -175
- package/docs/troubleshooting.html +86 -86
- package/package.json +165 -165
- package/scripts/auto-close-duplicates.ts +277 -0
- package/scripts/backfill-duplicate-comments.ts +213 -0
- package/scripts/codegraph.ts +221 -0
- package/scripts/comment-on-duplicates.sh +95 -0
- package/scripts/edit-issue-labels.sh +84 -0
- package/scripts/final-peer-rename.mjs +46 -0
- package/scripts/fix-encoding.mjs +83 -0
- package/scripts/fix-inconsistencies.mjs +67 -0
- package/scripts/generate-docs.ts +307 -0
- package/scripts/gh.sh +96 -0
- package/scripts/install.ps1 +37 -0
- package/scripts/install.sh +49 -0
- package/scripts/issue-lifecycle.ts +38 -0
- package/scripts/lifecycle-comment.ts +53 -0
- package/scripts/normalize-html.mjs +37 -0
- package/scripts/preload.ts +159 -0
- package/scripts/rename-content-peer-to-swarm.mjs +67 -0
- package/scripts/rename-hooks-mesh.mjs +6 -0
- package/scripts/rename-peer-to-swarm.mjs +62 -0
- package/scripts/run_devcontainer_claude_code.ps1 +152 -0
- package/scripts/scrape.py +82 -0
- package/scripts/session.ts +195 -0
- package/scripts/sweep.ts +168 -0
- package/scripts/write-discovery-test.cjs +93 -0
- package/scripts/write-discovery-test2.cjs +65 -0
- package/scripts/write-server-final.cjs +108 -0
- package/scripts/write-server-test.cjs +69 -0
- package/scripts/write-server-test2.cjs +99 -0
- package/scripts/write-server-test3.cjs +59 -0
- package/scripts/write-server-test4.cjs +108 -0
- package/scripts/write-server-v2.cjs +142 -0
- package/scripts/write-server-v2b.cjs +129 -0
- package/scripts/write-server-v2c.cjs +136 -0
- package/scripts/write-store-test.cjs +12 -0
- package/scripts/write-store-test2.cjs +163 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
var process: {
|
|
5
|
+
env: Record<string, string | undefined>;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface GitHubIssue {
|
|
10
|
+
number: number;
|
|
11
|
+
title: string;
|
|
12
|
+
user: { id: number };
|
|
13
|
+
created_at: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface GitHubComment {
|
|
17
|
+
id: number;
|
|
18
|
+
body: string;
|
|
19
|
+
created_at: string;
|
|
20
|
+
user: { type: string; id: number };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface GitHubReaction {
|
|
24
|
+
user: { id: number };
|
|
25
|
+
content: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function githubRequest<T>(endpoint: string, token: string, method: string = 'GET', body?: any): Promise<T> {
|
|
29
|
+
const response = await fetch(`https://api.github.com${endpoint}`, {
|
|
30
|
+
method,
|
|
31
|
+
headers: {
|
|
32
|
+
Authorization: `Bearer ${token}`,
|
|
33
|
+
Accept: "application/vnd.github.v3+json",
|
|
34
|
+
"User-Agent": "auto-close-duplicates-script",
|
|
35
|
+
...(body && { "Content-Type": "application/json" }),
|
|
36
|
+
},
|
|
37
|
+
...(body && { body: JSON.stringify(body) }),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`GitHub API request failed: ${response.status} ${response.statusText}`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return response.json();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function extractDuplicateIssueNumber(commentBody: string): number | null {
|
|
50
|
+
// Try to match #123 format first
|
|
51
|
+
let match = commentBody.match(/#(\d+)/);
|
|
52
|
+
if (match) {
|
|
53
|
+
return parseInt(match[1], 10);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Try to match GitHub issue URL format: https://github.com/owner/repo/issues/123
|
|
57
|
+
match = commentBody.match(/github\.com\/[^\/]+\/[^\/]+\/issues\/(\d+)/);
|
|
58
|
+
if (match) {
|
|
59
|
+
return parseInt(match[1], 10);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async function closeIssueAsDuplicate(
|
|
67
|
+
owner: string,
|
|
68
|
+
repo: string,
|
|
69
|
+
issueNumber: number,
|
|
70
|
+
duplicateOfNumber: number,
|
|
71
|
+
token: string
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
await githubRequest(
|
|
74
|
+
`/repos/${owner}/${repo}/issues/${issueNumber}`,
|
|
75
|
+
token,
|
|
76
|
+
'PATCH',
|
|
77
|
+
{
|
|
78
|
+
state: 'closed',
|
|
79
|
+
state_reason: 'duplicate',
|
|
80
|
+
labels: ['duplicate']
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
await githubRequest(
|
|
85
|
+
`/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
|
|
86
|
+
token,
|
|
87
|
+
'POST',
|
|
88
|
+
{
|
|
89
|
+
body: `This issue has been automatically closed as a duplicate of #${duplicateOfNumber}.
|
|
90
|
+
|
|
91
|
+
If this is incorrect, please re-open this issue or create a new one.
|
|
92
|
+
|
|
93
|
+
🤖 Generated with [Claude Code](https://claude.ai/code)`
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function autoCloseDuplicates(): Promise<void> {
|
|
100
|
+
console.log("[DEBUG] Starting auto-close duplicates script");
|
|
101
|
+
|
|
102
|
+
const token = process.env.GITHUB_TOKEN;
|
|
103
|
+
if (!token) {
|
|
104
|
+
throw new Error("GITHUB_TOKEN environment variable is required");
|
|
105
|
+
}
|
|
106
|
+
console.log("[DEBUG] GitHub token found");
|
|
107
|
+
|
|
108
|
+
const owner = process.env.GITHUB_REPOSITORY_OWNER || "anthropics";
|
|
109
|
+
const repo = process.env.GITHUB_REPOSITORY_NAME || "claude-code";
|
|
110
|
+
console.log(`[DEBUG] Repository: ${owner}/${repo}`);
|
|
111
|
+
|
|
112
|
+
const threeDaysAgo = new Date();
|
|
113
|
+
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
|
|
114
|
+
console.log(
|
|
115
|
+
`[DEBUG] Checking for duplicate comments older than: ${threeDaysAgo.toISOString()}`
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
console.log("[DEBUG] Fetching open issues created more than 3 days ago...");
|
|
119
|
+
const allIssues: GitHubIssue[] = [];
|
|
120
|
+
let page = 1;
|
|
121
|
+
const perPage = 100;
|
|
122
|
+
|
|
123
|
+
while (true) {
|
|
124
|
+
const pageIssues: GitHubIssue[] = await githubRequest(
|
|
125
|
+
`/repos/${owner}/${repo}/issues?state=open&per_page=${perPage}&page=${page}`,
|
|
126
|
+
token
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (pageIssues.length === 0) break;
|
|
130
|
+
|
|
131
|
+
// Filter for issues created more than 3 days ago
|
|
132
|
+
const oldEnoughIssues = pageIssues.filter(issue =>
|
|
133
|
+
new Date(issue.created_at) <= threeDaysAgo
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
allIssues.push(...oldEnoughIssues);
|
|
137
|
+
page++;
|
|
138
|
+
|
|
139
|
+
// Safety limit to avoid infinite loops
|
|
140
|
+
if (page > 20) break;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const issues = allIssues;
|
|
144
|
+
console.log(`[DEBUG] Found ${issues.length} open issues`);
|
|
145
|
+
|
|
146
|
+
let processedCount = 0;
|
|
147
|
+
let candidateCount = 0;
|
|
148
|
+
|
|
149
|
+
for (const issue of issues) {
|
|
150
|
+
processedCount++;
|
|
151
|
+
console.log(
|
|
152
|
+
`[DEBUG] Processing issue #${issue.number} (${processedCount}/${issues.length}): ${issue.title}`
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`);
|
|
156
|
+
const comments: GitHubComment[] = await githubRequest(
|
|
157
|
+
`/repos/${owner}/${repo}/issues/${issue.number}/comments`,
|
|
158
|
+
token
|
|
159
|
+
);
|
|
160
|
+
console.log(
|
|
161
|
+
`[DEBUG] Issue #${issue.number} has ${comments.length} comments`
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const dupeComments = comments.filter(
|
|
165
|
+
(comment) =>
|
|
166
|
+
comment.body.includes("Found") &&
|
|
167
|
+
comment.body.includes("possible duplicate") &&
|
|
168
|
+
comment.user.type === "Bot"
|
|
169
|
+
);
|
|
170
|
+
console.log(
|
|
171
|
+
`[DEBUG] Issue #${issue.number} has ${dupeComments.length} duplicate detection comments`
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (dupeComments.length === 0) {
|
|
175
|
+
console.log(
|
|
176
|
+
`[DEBUG] Issue #${issue.number} - no duplicate comments found, skipping`
|
|
177
|
+
);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const lastDupeComment = dupeComments[dupeComments.length - 1];
|
|
182
|
+
const dupeCommentDate = new Date(lastDupeComment.created_at);
|
|
183
|
+
console.log(
|
|
184
|
+
`[DEBUG] Issue #${
|
|
185
|
+
issue.number
|
|
186
|
+
} - most recent duplicate comment from: ${dupeCommentDate.toISOString()}`
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (dupeCommentDate > threeDaysAgo) {
|
|
190
|
+
console.log(
|
|
191
|
+
`[DEBUG] Issue #${issue.number} - duplicate comment is too recent, skipping`
|
|
192
|
+
);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
console.log(
|
|
196
|
+
`[DEBUG] Issue #${
|
|
197
|
+
issue.number
|
|
198
|
+
} - duplicate comment is old enough (${Math.floor(
|
|
199
|
+
(Date.now() - dupeCommentDate.getTime()) / (1000 * 60 * 60 * 24)
|
|
200
|
+
)} days)`
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const commentsAfterDupe = comments.filter(
|
|
204
|
+
(comment) => new Date(comment.created_at) > dupeCommentDate
|
|
205
|
+
);
|
|
206
|
+
console.log(
|
|
207
|
+
`[DEBUG] Issue #${issue.number} - ${commentsAfterDupe.length} comments after duplicate detection`
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
if (commentsAfterDupe.length > 0) {
|
|
211
|
+
console.log(
|
|
212
|
+
`[DEBUG] Issue #${issue.number} - has activity after duplicate comment, skipping`
|
|
213
|
+
);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log(
|
|
218
|
+
`[DEBUG] Issue #${issue.number} - checking reactions on duplicate comment...`
|
|
219
|
+
);
|
|
220
|
+
const reactions: GitHubReaction[] = await githubRequest(
|
|
221
|
+
`/repos/${owner}/${repo}/issues/comments/${lastDupeComment.id}/reactions`,
|
|
222
|
+
token
|
|
223
|
+
);
|
|
224
|
+
console.log(
|
|
225
|
+
`[DEBUG] Issue #${issue.number} - duplicate comment has ${reactions.length} reactions`
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const authorThumbsDown = reactions.some(
|
|
229
|
+
(reaction) =>
|
|
230
|
+
reaction.user.id === issue.user.id && reaction.content === "-1"
|
|
231
|
+
);
|
|
232
|
+
console.log(
|
|
233
|
+
`[DEBUG] Issue #${issue.number} - author thumbs down reaction: ${authorThumbsDown}`
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
if (authorThumbsDown) {
|
|
237
|
+
console.log(
|
|
238
|
+
`[DEBUG] Issue #${issue.number} - author disagreed with duplicate detection, skipping`
|
|
239
|
+
);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const duplicateIssueNumber = extractDuplicateIssueNumber(lastDupeComment.body);
|
|
244
|
+
if (!duplicateIssueNumber) {
|
|
245
|
+
console.log(
|
|
246
|
+
`[DEBUG] Issue #${issue.number} - could not extract duplicate issue number from comment, skipping`
|
|
247
|
+
);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
candidateCount++;
|
|
252
|
+
const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`;
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
console.log(
|
|
256
|
+
`[INFO] Auto-closing issue #${issue.number} as duplicate of #${duplicateIssueNumber}: ${issueUrl}`
|
|
257
|
+
);
|
|
258
|
+
await closeIssueAsDuplicate(owner, repo, issue.number, duplicateIssueNumber, token);
|
|
259
|
+
console.log(
|
|
260
|
+
`[SUCCESS] Successfully closed issue #${issue.number} as duplicate of #${duplicateIssueNumber}`
|
|
261
|
+
);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.error(
|
|
264
|
+
`[ERROR] Failed to close issue #${issue.number} as duplicate: ${error}`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
console.log(
|
|
270
|
+
`[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates for auto-close`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
autoCloseDuplicates().catch(console.error);
|
|
275
|
+
|
|
276
|
+
// Make it a module
|
|
277
|
+
export {};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
var process: {
|
|
5
|
+
env: Record<string, string | undefined>;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface GitHubIssue {
|
|
10
|
+
number: number;
|
|
11
|
+
title: string;
|
|
12
|
+
state: string;
|
|
13
|
+
state_reason?: string;
|
|
14
|
+
user: { id: number };
|
|
15
|
+
created_at: string;
|
|
16
|
+
closed_at?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface GitHubComment {
|
|
20
|
+
id: number;
|
|
21
|
+
body: string;
|
|
22
|
+
created_at: string;
|
|
23
|
+
user: { type: string; id: number };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function githubRequest<T>(endpoint: string, token: string, method: string = 'GET', body?: any): Promise<T> {
|
|
27
|
+
const response = await fetch(`https://api.github.com${endpoint}`, {
|
|
28
|
+
method,
|
|
29
|
+
headers: {
|
|
30
|
+
Authorization: `Bearer ${token}`,
|
|
31
|
+
Accept: "application/vnd.github.v3+json",
|
|
32
|
+
"User-Agent": "backfill-duplicate-comments-script",
|
|
33
|
+
...(body && { "Content-Type": "application/json" }),
|
|
34
|
+
},
|
|
35
|
+
...(body && { body: JSON.stringify(body) }),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`GitHub API request failed: ${response.status} ${response.statusText}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return response.json();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function triggerDedupeWorkflow(
|
|
48
|
+
owner: string,
|
|
49
|
+
repo: string,
|
|
50
|
+
issueNumber: number,
|
|
51
|
+
token: string,
|
|
52
|
+
dryRun: boolean = true
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
if (dryRun) {
|
|
55
|
+
console.log(`[DRY RUN] Would trigger dedupe workflow for issue #${issueNumber}`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await githubRequest(
|
|
60
|
+
`/repos/${owner}/${repo}/actions/workflows/claude-dedupe-issues.yml/dispatches`,
|
|
61
|
+
token,
|
|
62
|
+
'POST',
|
|
63
|
+
{
|
|
64
|
+
ref: 'main',
|
|
65
|
+
inputs: {
|
|
66
|
+
issue_number: issueNumber.toString()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function backfillDuplicateComments(): Promise<void> {
|
|
73
|
+
console.log("[DEBUG] Starting backfill duplicate comments script");
|
|
74
|
+
|
|
75
|
+
const token = process.env.GITHUB_TOKEN;
|
|
76
|
+
if (!token) {
|
|
77
|
+
throw new Error(`GITHUB_TOKEN environment variable is required
|
|
78
|
+
|
|
79
|
+
Usage:
|
|
80
|
+
GITHUB_TOKEN=your_token bun run scripts/backfill-duplicate-comments.ts
|
|
81
|
+
|
|
82
|
+
Environment Variables:
|
|
83
|
+
GITHUB_TOKEN - GitHub personal access token with repo and actions permissions (required)
|
|
84
|
+
DRY_RUN - Set to "false" to actually trigger workflows (default: true for safety)
|
|
85
|
+
MAX_ISSUE_NUMBER - Only process issues with numbers less than this value (default: 4050)`);
|
|
86
|
+
}
|
|
87
|
+
console.log("[DEBUG] GitHub token found");
|
|
88
|
+
|
|
89
|
+
const owner = "anthropics";
|
|
90
|
+
const repo = "claude-code";
|
|
91
|
+
const dryRun = process.env.DRY_RUN !== "false";
|
|
92
|
+
const maxIssueNumber = parseInt(process.env.MAX_ISSUE_NUMBER || "4050", 10);
|
|
93
|
+
const minIssueNumber = parseInt(process.env.MIN_ISSUE_NUMBER || "1", 10);
|
|
94
|
+
|
|
95
|
+
console.log(`[DEBUG] Repository: ${owner}/${repo}`);
|
|
96
|
+
console.log(`[DEBUG] Dry run mode: ${dryRun}`);
|
|
97
|
+
console.log(`[DEBUG] Looking at issues between #${minIssueNumber} and #${maxIssueNumber}`);
|
|
98
|
+
|
|
99
|
+
console.log(`[DEBUG] Fetching issues between #${minIssueNumber} and #${maxIssueNumber}...`);
|
|
100
|
+
const allIssues: GitHubIssue[] = [];
|
|
101
|
+
let page = 1;
|
|
102
|
+
const perPage = 100;
|
|
103
|
+
|
|
104
|
+
while (true) {
|
|
105
|
+
const pageIssues: GitHubIssue[] = await githubRequest(
|
|
106
|
+
`/repos/${owner}/${repo}/issues?state=all&per_page=${perPage}&page=${page}&sort=created&direction=desc`,
|
|
107
|
+
token
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (pageIssues.length === 0) break;
|
|
111
|
+
|
|
112
|
+
// Filter to only include issues within the specified range
|
|
113
|
+
const filteredIssues = pageIssues.filter(issue =>
|
|
114
|
+
issue.number >= minIssueNumber && issue.number < maxIssueNumber
|
|
115
|
+
);
|
|
116
|
+
allIssues.push(...filteredIssues);
|
|
117
|
+
|
|
118
|
+
// If the oldest issue in this page is still above our minimum, we need to continue
|
|
119
|
+
// but if the oldest issue is below our minimum, we can stop
|
|
120
|
+
const oldestIssueInPage = pageIssues[pageIssues.length - 1];
|
|
121
|
+
if (oldestIssueInPage && oldestIssueInPage.number >= maxIssueNumber) {
|
|
122
|
+
console.log(`[DEBUG] Oldest issue in page #${page} is #${oldestIssueInPage.number}, continuing...`);
|
|
123
|
+
} else if (oldestIssueInPage && oldestIssueInPage.number < minIssueNumber) {
|
|
124
|
+
console.log(`[DEBUG] Oldest issue in page #${page} is #${oldestIssueInPage.number}, below minimum, stopping`);
|
|
125
|
+
break;
|
|
126
|
+
} else if (filteredIssues.length === 0 && pageIssues.length > 0) {
|
|
127
|
+
console.log(`[DEBUG] No issues in page #${page} are in range #${minIssueNumber}-#${maxIssueNumber}, continuing...`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
page++;
|
|
131
|
+
|
|
132
|
+
// Safety limit to avoid infinite loops
|
|
133
|
+
if (page > 200) {
|
|
134
|
+
console.log("[DEBUG] Reached page limit, stopping pagination");
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log(`[DEBUG] Found ${allIssues.length} issues between #${minIssueNumber} and #${maxIssueNumber}`);
|
|
140
|
+
|
|
141
|
+
let processedCount = 0;
|
|
142
|
+
let candidateCount = 0;
|
|
143
|
+
let triggeredCount = 0;
|
|
144
|
+
|
|
145
|
+
for (const issue of allIssues) {
|
|
146
|
+
processedCount++;
|
|
147
|
+
console.log(
|
|
148
|
+
`[DEBUG] Processing issue #${issue.number} (${processedCount}/${allIssues.length}): ${issue.title}`
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`);
|
|
152
|
+
const comments: GitHubComment[] = await githubRequest(
|
|
153
|
+
`/repos/${owner}/${repo}/issues/${issue.number}/comments`,
|
|
154
|
+
token
|
|
155
|
+
);
|
|
156
|
+
console.log(
|
|
157
|
+
`[DEBUG] Issue #${issue.number} has ${comments.length} comments`
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Look for existing duplicate detection comments (from the dedupe bot)
|
|
161
|
+
const dupeDetectionComments = comments.filter(
|
|
162
|
+
(comment) =>
|
|
163
|
+
comment.body.includes("Found") &&
|
|
164
|
+
comment.body.includes("possible duplicate") &&
|
|
165
|
+
comment.user.type === "Bot"
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
console.log(
|
|
169
|
+
`[DEBUG] Issue #${issue.number} has ${dupeDetectionComments.length} duplicate detection comments`
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Skip if there's already a duplicate detection comment
|
|
173
|
+
if (dupeDetectionComments.length > 0) {
|
|
174
|
+
console.log(
|
|
175
|
+
`[DEBUG] Issue #${issue.number} already has duplicate detection comment, skipping`
|
|
176
|
+
);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
candidateCount++;
|
|
181
|
+
const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`;
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
console.log(
|
|
185
|
+
`[INFO] ${dryRun ? '[DRY RUN] ' : ''}Triggering dedupe workflow for issue #${issue.number}: ${issueUrl}`
|
|
186
|
+
);
|
|
187
|
+
await triggerDedupeWorkflow(owner, repo, issue.number, token, dryRun);
|
|
188
|
+
|
|
189
|
+
if (!dryRun) {
|
|
190
|
+
console.log(
|
|
191
|
+
`[SUCCESS] Successfully triggered dedupe workflow for issue #${issue.number}`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
triggeredCount++;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error(
|
|
197
|
+
`[ERROR] Failed to trigger workflow for issue #${issue.number}: ${error}`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Add a delay between workflow triggers to avoid overwhelming the system
|
|
202
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log(
|
|
206
|
+
`[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates without duplicate comments, ${dryRun ? 'would trigger' : 'triggered'} ${triggeredCount} workflows`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
backfillDuplicateComments().catch(console.error);
|
|
211
|
+
|
|
212
|
+
// Make it a module
|
|
213
|
+
export {};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Code Graph Generator
|
|
4
|
+
*
|
|
5
|
+
* Scans src/ and builds a persistent structure map of the codebase.
|
|
6
|
+
* Uses ripgrep (fast, available in the project) for text patterns.
|
|
7
|
+
*
|
|
8
|
+
* Usage: bun run scripts/codegraph.ts
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs'
|
|
12
|
+
import { join, dirname } from 'path'
|
|
13
|
+
import { execSync } from 'child_process'
|
|
14
|
+
|
|
15
|
+
const ROOT = join(import.meta.dirname, '..')
|
|
16
|
+
const SRC = join(ROOT, 'src')
|
|
17
|
+
const OUT = join(ROOT, '.clew', 'CODEGRAPH.md')
|
|
18
|
+
|
|
19
|
+
function rg(args: string): string {
|
|
20
|
+
try {
|
|
21
|
+
return execSync(`rg ${args}`, { cwd: ROOT, encoding: 'utf-8', timeout: 60000, stdio: ['pipe', 'pipe', 'ignore'] })
|
|
22
|
+
} catch (e: any) {
|
|
23
|
+
return e.stdout || ''
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface Module {
|
|
28
|
+
path: string
|
|
29
|
+
lines: number
|
|
30
|
+
exports: string[]
|
|
31
|
+
types: string[]
|
|
32
|
+
classNames: string[]
|
|
33
|
+
imports: string[]
|
|
34
|
+
todos: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── 1. File list ──
|
|
38
|
+
console.log('📁 Scanning files...')
|
|
39
|
+
const fileLines = rg('--count --sort path --type ts "^" src/').trim().split('\n').filter(Boolean)
|
|
40
|
+
|
|
41
|
+
const files = new Map<string, Module>()
|
|
42
|
+
|
|
43
|
+
for (const entry of fileLines) {
|
|
44
|
+
const colon = entry.lastIndexOf(':')
|
|
45
|
+
const relPath = entry.slice(0, colon).replace(/\\/g, '/').replace(/^src\//, '')
|
|
46
|
+
const lineCount = parseInt(entry.slice(colon + 1)) || 0
|
|
47
|
+
if (!relPath || relPath.startsWith('..')) continue
|
|
48
|
+
|
|
49
|
+
files.set(relPath, {
|
|
50
|
+
path: relPath,
|
|
51
|
+
lines: lineCount,
|
|
52
|
+
exports: [],
|
|
53
|
+
types: [],
|
|
54
|
+
classNames: [],
|
|
55
|
+
imports: [],
|
|
56
|
+
todos: 0,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
console.log(` ${files.size} files`)
|
|
60
|
+
|
|
61
|
+
// ── 2. Exports via ripgrep (bulk) ──
|
|
62
|
+
console.log('🔍 Exports...')
|
|
63
|
+
|
|
64
|
+
type Field = 'exports' | 'types' | 'classNames'
|
|
65
|
+
const patterns: { rgPattern: string; field: Field }[] = [
|
|
66
|
+
{ rgPattern: 'export (async )?function [a-zA-Z_$][a-zA-Z0-9_$]*', field: 'exports' },
|
|
67
|
+
{ rgPattern: 'export const [a-zA-Z_$][a-zA-Z0-9_$]*', field: 'exports' },
|
|
68
|
+
{ rgPattern: 'export (abstract )?class [a-zA-Z_$][a-zA-Z0-9_$]*', field: 'classNames' },
|
|
69
|
+
{ rgPattern: 'export interface [a-zA-Z_$][a-zA-Z0-9_$]*', field: 'types' },
|
|
70
|
+
{ rgPattern: 'export type [a-zA-Z_$][a-zA-Z0-9_$]*', field: 'types' },
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
for (const p of patterns) {
|
|
74
|
+
// Single rg call per pattern — fast
|
|
75
|
+
const out = rg(`--with-filename --only-matching --no-line-number "${p.rgPattern}" src/`).trim()
|
|
76
|
+
for (const line of out.split('\n').filter(Boolean)) {
|
|
77
|
+
const fileSep = line.indexOf(':')
|
|
78
|
+
if (fileSep === -1) continue
|
|
79
|
+
const rawPath = line.slice(0, fileSep).replace(/\\/g, '/').replace(/^src\//, '')
|
|
80
|
+
const matched = line.slice(fileSep + 1).trim()
|
|
81
|
+
const name = matched.split(/\s+/).pop()
|
|
82
|
+
if (!name || !rawPath) continue
|
|
83
|
+
const f = files.get(rawPath)
|
|
84
|
+
if (f && !(f[p.field] as string[]).includes(name)) {
|
|
85
|
+
(f[p.field] as string[]).push(name)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── 3. Imports + TODOs ──
|
|
91
|
+
console.log('📎 Dependencies...')
|
|
92
|
+
|
|
93
|
+
for (const [relPath, info] of files) {
|
|
94
|
+
try {
|
|
95
|
+
const content = readFileSync(join(SRC, relPath), 'utf-8')
|
|
96
|
+
|
|
97
|
+
// Internal imports
|
|
98
|
+
const srcImports: string[] = []
|
|
99
|
+
for (const line of content.split('\n')) {
|
|
100
|
+
const m = line.match(/from\s+['"](src\/[^'"]+)['"]/)
|
|
101
|
+
if (m) srcImports.push(m[1])
|
|
102
|
+
}
|
|
103
|
+
info.imports = [...new Set(srcImports)]
|
|
104
|
+
|
|
105
|
+
// TODOs
|
|
106
|
+
info.todos = (content.match(/\b(TODO|FIXME|HACK|XXX)\b/g) || []).length
|
|
107
|
+
} catch {
|
|
108
|
+
// skip unreadable files
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Also add class names to exports
|
|
113
|
+
for (const f of files.values()) {
|
|
114
|
+
for (const cls of f.classNames) {
|
|
115
|
+
if (!f.exports.includes(cls)) f.exports.push(cls)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── 4. Generate markdown ──
|
|
120
|
+
console.log('📝 Generating...')
|
|
121
|
+
|
|
122
|
+
const totalLines = [...files.values()].reduce((a, f) => a + f.lines, 0)
|
|
123
|
+
const totalExports = [...files.values()].reduce((a, f) => a + f.exports.length, 0)
|
|
124
|
+
const totalTodos = [...files.values()].reduce((a, f) => a + f.todos, 0)
|
|
125
|
+
|
|
126
|
+
const md: string[] = []
|
|
127
|
+
md.push('# Code Graph')
|
|
128
|
+
md.push('')
|
|
129
|
+
md.push(`_Auto-generated ${new Date().toISOString().slice(0, 10)}_`)
|
|
130
|
+
md.push('')
|
|
131
|
+
md.push(`- **Source files**: ${files.size}`)
|
|
132
|
+
md.push(`- **Total lines**: ${totalLines.toLocaleString()}`)
|
|
133
|
+
md.push(`- **Exports**: ${totalExports}`)
|
|
134
|
+
md.push(`- **TODOs**: ${totalTodos}`)
|
|
135
|
+
md.push('')
|
|
136
|
+
|
|
137
|
+
// Group by top directory
|
|
138
|
+
const dirMap = new Map<string, Module[]>()
|
|
139
|
+
for (const f of files.values()) {
|
|
140
|
+
const dir = f.path.split('/')[0]
|
|
141
|
+
if (!dirMap.has(dir)) dirMap.set(dir, [])
|
|
142
|
+
dirMap.get(dir)!.push(f)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const [dir, dirFiles] of [...dirMap.entries()].sort()) {
|
|
146
|
+
md.push(`## ${dir}/`)
|
|
147
|
+
md.push('')
|
|
148
|
+
dirFiles.sort((a, b) => b.lines - a.lines)
|
|
149
|
+
|
|
150
|
+
for (const f of dirFiles) {
|
|
151
|
+
if (f.lines < 25 && !f.exports.length && !f.types.length) continue
|
|
152
|
+
|
|
153
|
+
const badges = [`${f.lines} lines`]
|
|
154
|
+
if (f.todos) badges.push(`⚠️ ${f.todos}`)
|
|
155
|
+
|
|
156
|
+
md.push(`### \`${f.path}\``)
|
|
157
|
+
md.push(`_${badges.join(', ')}_`)
|
|
158
|
+
|
|
159
|
+
if (f.exports.length) {
|
|
160
|
+
const list = f.exports.slice(0, 10).join(', ')
|
|
161
|
+
md.push(`- exports: ${list}${f.exports.length > 10 ? ` [+${f.exports.length - 10}]` : ''}`)
|
|
162
|
+
}
|
|
163
|
+
if (f.types.length) {
|
|
164
|
+
const list = f.types.slice(0, 6).join(', ')
|
|
165
|
+
md.push(`- types: ${list}${f.types.length > 6 ? ` [+${f.types.length - 6}]` : ''}`)
|
|
166
|
+
}
|
|
167
|
+
if (f.imports.length) {
|
|
168
|
+
const list = f.imports.slice(0, 5).join(', ')
|
|
169
|
+
md.push(`- deps: ${list}${f.imports.length > 5 ? ` (+${f.imports.length - 5})` : ''}`)
|
|
170
|
+
}
|
|
171
|
+
md.push('')
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Hot dependencies
|
|
176
|
+
const depCount = new Map<string, number>()
|
|
177
|
+
for (const f of files.values()) {
|
|
178
|
+
for (const dep of f.imports) {
|
|
179
|
+
depCount.set(dep, (depCount.get(dep) || 0) + 1)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const sortedDeps = [...depCount.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15)
|
|
184
|
+
if (sortedDeps.length) {
|
|
185
|
+
md.push('---')
|
|
186
|
+
md.push('## Hot Dependencies')
|
|
187
|
+
md.push('')
|
|
188
|
+
for (const [dep, count] of sortedDeps) {
|
|
189
|
+
md.push(`- \`${dep}\` — imported by ${count} files`)
|
|
190
|
+
}
|
|
191
|
+
md.push('')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Large files
|
|
195
|
+
const largeFiles = [...files.values()].filter(f => f.lines > 300).sort((a, b) => b.lines - a.lines)
|
|
196
|
+
if (largeFiles.length) {
|
|
197
|
+
md.push('---')
|
|
198
|
+
md.push('## Large Files (>300 lines)')
|
|
199
|
+
md.push('')
|
|
200
|
+
for (const f of largeFiles) {
|
|
201
|
+
md.push(`- \`${f.path}\` — ${f.lines} lines${f.todos ? ', ⚠️' : ''}`)
|
|
202
|
+
}
|
|
203
|
+
md.push('')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// TODO hotspots
|
|
207
|
+
const todoFiles = [...files.values()].filter(f => f.todos > 2).sort((a, b) => b.todos - a.todos).slice(0, 10)
|
|
208
|
+
if (todoFiles.length) {
|
|
209
|
+
md.push('---')
|
|
210
|
+
md.push('## TODO Hotspots')
|
|
211
|
+
md.push('')
|
|
212
|
+
for (const f of todoFiles) {
|
|
213
|
+
md.push(`- \`${f.path}\` — ${f.todos} TODOs`)
|
|
214
|
+
}
|
|
215
|
+
md.push('')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
mkdirSync(dirname(OUT), { recursive: true })
|
|
219
|
+
writeFileSync(OUT, md.join('\n'), 'utf-8')
|
|
220
|
+
console.log(`\n✅ ${OUT}`)
|
|
221
|
+
console.log(` ${files.size} files | ${totalLines.toLocaleString()} lines | ${totalExports} exports`)
|