spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

claude-code / scripts / sweep.ts
106 lines · 3.7 KBRaw
1#!/usr/bin/env bun
2
3// --
4
5const NEW_ISSUE = "https://github.com/anthropics/claude-code/issues/new/choose";
6const DRY_RUN = process.argv.includes("--dry-run");
7
8const lifecycle = [
9 { label: "needs-repro", days: 7 },
10 { label: "needs-info", days: 7 },
11 { label: "needs-votes", days: 30 },
12 { label: "stale", days: 30 },
13];
14
15const closeMessages: Record<string, string> = {
16 "needs-repro": `Closing — we weren't able to get the reproduction steps needed to investigate.\n\nIf this is still a problem, please [open a new issue](${NEW_ISSUE}) with steps to reproduce.`,
17 "needs-info": `Closing — we didn't receive the information needed to move forward.\n\nIf this is still a problem, please [open a new issue](${NEW_ISSUE}) with the requested details.`,
18 "needs-votes": `Closing this feature request — it didn't get enough community support to prioritize.\n\nIf you'd still like to see this, please [open a new feature request](${NEW_ISSUE}) with more context about the use case.`,
19 stale: `Closing due to inactivity.\n\nIf this is still a problem, please [open a new issue](${NEW_ISSUE}) with up-to-date information.`,
20};
21
22// --
23
24async function githubRequest<T>(
25 endpoint: string,
26 method = "GET",
27 body?: unknown
28): Promise<T> {
29 const token = process.env.GITHUB_TOKEN;
30 if (!token) throw new Error("GITHUB_TOKEN required");
31
32 const response = await fetch(`https://api.github.com${endpoint}`, {
33 method,
34 headers: {
35 Authorization: `Bearer ${token}`,
36 Accept: "application/vnd.github.v3+json",
37 "User-Agent": "sweep",
38 ...(body && { "Content-Type": "application/json" }),
39 },
40 ...(body && { body: JSON.stringify(body) }),
41 });
42
43 if (!response.ok) {
44 if (response.status === 404) return {} as T;
45 const text = await response.text();
46 throw new Error(`GitHub API ${response.status}: ${text}`);
47 }
48
49 return response.json();
50}
51
52// --
53
54async function main() {
55 const owner = process.env.GITHUB_REPOSITORY_OWNER;
56 const repo = process.env.GITHUB_REPOSITORY_NAME;
57 if (!owner || !repo)
58 throw new Error("GITHUB_REPOSITORY_OWNER and GITHUB_REPOSITORY_NAME required");
59
60 if (DRY_RUN) console.log("DRY RUN — no issues will be closed\n");
61
62 let closed = 0;
63
64 for (const { label, days } of lifecycle) {
65 const cutoff = new Date();
66 cutoff.setDate(cutoff.getDate() - days);
67 console.log(`\n=== ${label} (${days}d timeout) ===`);
68
69 for (let page = 1; page <= 10; page++) {
70 const issues = await githubRequest<any[]>(
71 `/repos/${owner}/${repo}/issues?state=open&labels=${label}&sort=updated&direction=asc&per_page=100&page=${page}`
72 );
73 if (issues.length === 0) break;
74
75 for (const issue of issues) {
76 if (issue.pull_request) continue;
77 const base = `/repos/${owner}/${repo}/issues/${issue.number}`;
78
79 const events = await githubRequest<any[]>(`${base}/events?per_page=100`);
80
81 const labeledAt = events
82 .filter((e) => e.event === "labeled" && e.label?.name === label)
83 .map((e) => new Date(e.created_at))
84 .pop();
85
86 if (!labeledAt || labeledAt > cutoff) continue;
87
88 if (DRY_RUN) {
89 const age = Math.floor((Date.now() - labeledAt.getTime()) / 86400000);
90 console.log(`#${issue.number}: would close (${label}, ${age}d old) — ${issue.title}`);
91 } else {
92 await githubRequest(`${base}/comments`, "POST", { body: closeMessages[label] });
93 await githubRequest(base, "PATCH", { state: "closed", state_reason: "not_planned" });
94 console.log(`#${issue.number}: closed (${label})`);
95 }
96 closed++;
97 }
98 }
99 }
100
101 console.log(`\nDone: ${closed} ${DRY_RUN ? "would be closed" : "closed"}`);
102}
103
104main().catch(console.error);
105
106export {};