#!/usr/bin/env bun declare global { var process: { env: Record; }; } interface GitHubIssue { number: number; title: string; user: { id: number }; } interface GitHubComment { id: number; body: string; created_at: string; user: { type: string; id: number }; } interface GitHubReaction { user: { id: number }; content: string; } async function githubRequest(endpoint: string, token: string): Promise { const response = await fetch(`https://api.github.com${endpoint}`, { headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github.v3+json", "User-Agent": "auto-close-duplicates-script", }, }); if (!response.ok) { throw new Error( `GitHub API request failed: ${response.status} ${response.statusText}` ); } return response.json(); } async function autoCloseDuplicates(): Promise { console.log("[DEBUG] Starting auto-close duplicates script"); const token = process.env.GITHUB_TOKEN; if (!token) { throw new Error("GITHUB_TOKEN environment variable is required"); } console.log("[DEBUG] GitHub token found"); const owner = process.env.GITHUB_REPOSITORY_OWNER || "anthropics"; const repo = process.env.GITHUB_REPOSITORY_NAME || "claude-code"; console.log(`[DEBUG] Repository: ${owner}/${repo}`); const threeDaysAgo = new Date(); threeDaysAgo.setDate(threeDaysAgo.getDate() - 3); console.log( `[DEBUG] Checking for duplicate comments older than: ${threeDaysAgo.toISOString()}` ); console.log("[DEBUG] Fetching open issues..."); const issues: GitHubIssue[] = await githubRequest( `/repos/${owner}/${repo}/issues?state=open&per_page=100`, token ); console.log(`[DEBUG] Found ${issues.length} open issues`); let processedCount = 0; let candidateCount = 0; for (const issue of issues) { processedCount++; console.log( `[DEBUG] Processing issue #${issue.number} (${processedCount}/${issues.length}): ${issue.title}` ); console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`); const comments: GitHubComment[] = await githubRequest( `/repos/${owner}/${repo}/issues/${issue.number}/comments`, token ); console.log( `[DEBUG] Issue #${issue.number} has ${comments.length} comments` ); const dupeComments = comments.filter( (comment) => comment.body.includes("Found") && comment.body.includes("possible duplicate") && comment.user.type === "Bot" ); console.log( `[DEBUG] Issue #${issue.number} has ${dupeComments.length} duplicate detection comments` ); if (dupeComments.length === 0) { console.log( `[DEBUG] Issue #${issue.number} - no duplicate comments found, skipping` ); continue; } const lastDupeComment = dupeComments[dupeComments.length - 1]; const dupeCommentDate = new Date(lastDupeComment.created_at); console.log( `[DEBUG] Issue #${ issue.number } - most recent duplicate comment from: ${dupeCommentDate.toISOString()}` ); if (dupeCommentDate > threeDaysAgo) { console.log( `[DEBUG] Issue #${issue.number} - duplicate comment is too recent, skipping` ); continue; } console.log( `[DEBUG] Issue #${ issue.number } - duplicate comment is old enough (${Math.floor( (Date.now() - dupeCommentDate.getTime()) / (1000 * 60 * 60 * 24) )} days)` ); const commentsAfterDupe = comments.filter( (comment) => new Date(comment.created_at) > dupeCommentDate ); console.log( `[DEBUG] Issue #${issue.number} - ${commentsAfterDupe.length} comments after duplicate detection` ); if (commentsAfterDupe.length > 0) { console.log( `[DEBUG] Issue #${issue.number} - has activity after duplicate comment, skipping` ); continue; } console.log( `[DEBUG] Issue #${issue.number} - checking reactions on duplicate comment...` ); const reactions: GitHubReaction[] = await githubRequest( `/repos/${owner}/${repo}/issues/comments/${lastDupeComment.id}/reactions`, token ); console.log( `[DEBUG] Issue #${issue.number} - duplicate comment has ${reactions.length} reactions` ); const authorThumbsDown = reactions.some( (reaction) => reaction.user.id === issue.user.id && reaction.content === "-1" ); console.log( `[DEBUG] Issue #${issue.number} - author thumbs down reaction: ${authorThumbsDown}` ); if (authorThumbsDown) { console.log( `[DEBUG] Issue #${issue.number} - author disagreed with duplicate detection, skipping` ); continue; } candidateCount++; const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`; console.log( `[DRY RUN] Would auto-close issue #${issue.number} as duplicate: ${issueUrl}` ); } console.log( `[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates for auto-close` ); } autoCloseDuplicates().catch(console.error); // Make it a module export {};