gitpadi 2.0.0 โ†’ 2.0.2

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 CHANGED
@@ -264,3 +264,7 @@ npx gitpadi --owner MyOrg --repo my-project --token ghp_xxx issues list
264
264
  ## ๐Ÿ“„ License
265
265
 
266
266
  MIT โ€” Built by [Netwalls](https://github.com/Netwalls)
267
+
268
+
269
+
270
+ i noticed that a users have more issues but it is just bring out the top 10 issues that are open how do we bring out more issues
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+ // applicant-scorer.ts โ€” Detects issue applicants, scores them, and recommends the best fit
3
+ //
4
+ // Works for ANY repository โ€” no hardcoded values.
5
+ //
6
+ // Usage via Action: action: score-applicant
7
+ // Usage (CLI): GITHUB_TOKEN=xxx ISSUE_NUMBER=5 COMMENT_ID=123 npx tsx src/applicant-scorer.ts
8
+ import { Octokit } from '@octokit/rest';
9
+ // โ”€โ”€ Config โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
10
+ const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
11
+ const OWNER = process.env.GITHUB_OWNER || process.env.GITHUB_REPOSITORY?.split('/')[0] || '';
12
+ const REPO = process.env.GITHUB_REPO || process.env.GITHUB_REPOSITORY?.split('/')[1] || '';
13
+ const ISSUE_NUMBER = parseInt(process.env.ISSUE_NUMBER || '0');
14
+ const COMMENT_ID = parseInt(process.env.COMMENT_ID || '0');
15
+ const NOTIFY_USER = process.env.NOTIFY_USER || '';
16
+ if (!GITHUB_TOKEN) {
17
+ console.error('โŒ GITHUB_TOKEN required');
18
+ process.exit(1);
19
+ }
20
+ const octokit = new Octokit({ auth: GITHUB_TOKEN });
21
+ // โ”€โ”€ Application Detection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
22
+ const APPLICATION_PATTERNS = [
23
+ /i('d| would) (like|love|want) to (work on|tackle|take|pick up|handle)/i,
24
+ /can i (work on|take|pick up|handle|be assigned)/i,
25
+ /assign (this |it )?(to )?me/i,
26
+ /i('m| am) interested/i,
27
+ /let me (work on|take|handle)/i,
28
+ /i('ll| will) (work on|take|handle|do)/i,
29
+ /i want to contribute/i,
30
+ /please assign/i,
31
+ /i can (do|handle|take care of|work on)/i,
32
+ /picking this up/i,
33
+ /claiming this/i,
34
+ ];
35
+ function isApplicationComment(body) {
36
+ return APPLICATION_PATTERNS.some((p) => p.test(body));
37
+ }
38
+ // โ”€โ”€ Fetch Profile โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
39
+ async function fetchProfile(username, commentBody) {
40
+ const { data: user } = await octokit.users.getByUsername({ username });
41
+ const accountAge = Math.floor((Date.now() - new Date(user.created_at).getTime()) / 86400000);
42
+ let totalContributions = 0;
43
+ try {
44
+ const { data: events } = await octokit.activity.listPublicEventsForUser({ username, per_page: 100 });
45
+ totalContributions = events.length;
46
+ }
47
+ catch { /* continue */ }
48
+ let prsMerged = 0, prsOpen = 0, issuesCreated = 0;
49
+ try {
50
+ const { data: mp } = await octokit.search.issuesAndPullRequests({ q: `repo:${OWNER}/${REPO} type:pr author:${username} is:merged` });
51
+ prsMerged = mp.total_count;
52
+ const { data: op } = await octokit.search.issuesAndPullRequests({ q: `repo:${OWNER}/${REPO} type:pr author:${username} is:open` });
53
+ prsOpen = op.total_count;
54
+ const { data: ci } = await octokit.search.issuesAndPullRequests({ q: `repo:${OWNER}/${REPO} type:issue author:${username}` });
55
+ issuesCreated = ci.total_count;
56
+ }
57
+ catch { /* rate limit */ }
58
+ let relevantLanguages = [];
59
+ try {
60
+ const { data: repos } = await octokit.repos.listForUser({ username, sort: 'updated', per_page: 20 });
61
+ const langs = new Set();
62
+ repos.forEach((r) => { if (r.language)
63
+ langs.add(r.language); });
64
+ relevantLanguages = Array.from(langs);
65
+ }
66
+ catch { /* continue */ }
67
+ let hasReadme = false;
68
+ try {
69
+ await octokit.repos.get({ owner: username, repo: username });
70
+ hasReadme = true;
71
+ }
72
+ catch { /* no readme */ }
73
+ return {
74
+ username, avatarUrl: user.avatar_url, accountAge,
75
+ publicRepos: user.public_repos, followers: user.followers,
76
+ totalContributions, repoContributions: prsMerged + prsOpen + issuesCreated,
77
+ relevantLanguages, hasReadme, prsMerged, prsOpen, issuesCreated, commentBody,
78
+ };
79
+ }
80
+ // โ”€โ”€ Scoring Algorithm โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
81
+ function scoreApplicant(p, issueLabels) {
82
+ const b = { accountMaturity: 0, repoExperience: 0, githubPresence: 0, activityLevel: 0, applicationQuality: 0, languageRelevance: 0, total: 0 };
83
+ // 1. Account Maturity (0-15)
84
+ b.accountMaturity = p.accountAge > 730 ? 15 : p.accountAge > 365 ? 12 : p.accountAge > 180 ? 9 : p.accountAge > 90 ? 6 : p.accountAge > 30 ? 3 : 1;
85
+ // 2. Repo Experience (0-30) โ€” highest weight
86
+ b.repoExperience = Math.min(15, p.prsMerged * 5) + Math.min(5, p.prsOpen * 3) + Math.min(5, p.issuesCreated * 2) + Math.min(5, p.repoContributions);
87
+ // 3. GitHub Presence (0-15)
88
+ b.githubPresence = Math.min(5, Math.floor(p.publicRepos / 5)) + Math.min(5, Math.floor(p.followers / 10)) + (p.hasReadme ? 5 : 0);
89
+ // 4. Activity Level (0-15)
90
+ b.activityLevel = p.totalContributions >= 80 ? 15 : p.totalContributions >= 50 ? 12 : p.totalContributions >= 30 ? 9 : p.totalContributions >= 15 ? 6 : p.totalContributions >= 5 ? 3 : 1;
91
+ // 5. Application Quality (0-15)
92
+ const words = p.commentBody.split(/\s+/).length;
93
+ const hasApproach = /approach|plan|implement|would|will|by|using|step/i.test(p.commentBody);
94
+ const hasExp = /experience|worked|built|familiar|know|background/i.test(p.commentBody);
95
+ b.applicationQuality = (words >= 50 && hasApproach && hasExp) ? 15 : (words >= 30 && (hasApproach || hasExp)) ? 12 : (words >= 20 && hasApproach) ? 9 : words >= 15 ? 6 : words >= 8 ? 3 : 1;
96
+ // 6. Language Relevance (0-10) โ€” auto-detect from repo languages
97
+ b.languageRelevance = 5; // default neutral
98
+ if (issueLabels.length > 0 && p.relevantLanguages.length > 0) {
99
+ // Approximate: check if user knows popular labels' likely languages
100
+ const labelLower = issueLabels.map((l) => l.toLowerCase()).join(' ');
101
+ const hasRust = p.relevantLanguages.includes('Rust');
102
+ const hasTS = p.relevantLanguages.includes('TypeScript') || p.relevantLanguages.includes('JavaScript');
103
+ const needsRust = /contract|rust|wasm|soroban/i.test(labelLower);
104
+ const needsTS = /backend|frontend|api|websocket|typescript/i.test(labelLower);
105
+ let matches = 0, needed = 0;
106
+ if (needsRust) {
107
+ needed++;
108
+ if (hasRust)
109
+ matches++;
110
+ }
111
+ if (needsTS) {
112
+ needed++;
113
+ if (hasTS)
114
+ matches++;
115
+ }
116
+ if (needed > 0)
117
+ b.languageRelevance = Math.round((matches / needed) * 10);
118
+ }
119
+ b.total = b.accountMaturity + b.repoExperience + b.githubPresence + b.activityLevel + b.applicationQuality + b.languageRelevance;
120
+ const tier = b.total >= 75 ? 'S' : b.total >= 55 ? 'A' : b.total >= 40 ? 'B' : b.total >= 25 ? 'C' : 'D';
121
+ return { ...p, score: b.total, breakdown: b, tier };
122
+ }
123
+ // โ”€โ”€ Build Comments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
124
+ const TIER_EMOJI = { S: '๐Ÿ†', A: '๐ŸŸข', B: '๐ŸŸก', C: '๐ŸŸ ', D: '๐Ÿ”ด' };
125
+ function buildReviewComment(s, count) {
126
+ let c = `## ๐Ÿค– Contributor Agent โ€” Applicant Review\n\n`;
127
+ c += `**Applicant:** @${s.username} ${TIER_EMOJI[s.tier]} **Tier ${s.tier}** (${s.score}/100)\n\n`;
128
+ c += `<details>\n<summary>๐Ÿ“Š Score Breakdown</summary>\n\n`;
129
+ c += `| Category | Score | Max |\n|----------|-------|-----|\n`;
130
+ c += `| ๐Ÿ›๏ธ Account Maturity | ${s.breakdown.accountMaturity} | 15 |\n`;
131
+ c += `| ๐Ÿ”ง Repo Experience | ${s.breakdown.repoExperience} | 30 |\n`;
132
+ c += `| ๐ŸŒ GitHub Presence | ${s.breakdown.githubPresence} | 15 |\n`;
133
+ c += `| โšก Activity Level | ${s.breakdown.activityLevel} | 15 |\n`;
134
+ c += `| ๐Ÿ“ Application Quality | ${s.breakdown.applicationQuality} | 15 |\n`;
135
+ c += `| ๐Ÿ’ป Language Relevance | ${s.breakdown.languageRelevance} | 10 |\n`;
136
+ c += `| **Total** | **${s.score}** | **100** |\n\n`;
137
+ c += `**Profile:** ${s.publicRepos} repos ยท ${s.followers} followers ยท ${Math.round(s.accountAge / 30)} months\n`;
138
+ c += `**Languages:** ${s.relevantLanguages.slice(0, 8).join(', ') || 'None detected'}\n`;
139
+ c += `**Repo history:** ${s.prsMerged} merged PRs ยท ${s.prsOpen} open PRs ยท ${s.issuesCreated} issues\n\n`;
140
+ c += `</details>\n\n`;
141
+ if (s.tier === 'S' || s.tier === 'A')
142
+ c += `> โœ… **Strong candidate.** Consider assigning.\n`;
143
+ else if (s.tier === 'B')
144
+ c += `> ๐ŸŸก **Decent candidate.** Review profile before assigning.\n`;
145
+ else
146
+ c += `> โš ๏ธ **Low score.** Wait for stronger candidates or review manually.\n`;
147
+ if (count > 1)
148
+ c += `\n๐Ÿ“‹ **${count} applicant(s)** โ€” see comparison below.\n`;
149
+ if (NOTIFY_USER)
150
+ c += `\n๐Ÿ”” cc @${NOTIFY_USER}\n`;
151
+ c += `\n---\n<sub>๐Ÿค– Contributor Agent ยท Tier ${s.tier} ยท ${s.score}/100</sub>`;
152
+ return c;
153
+ }
154
+ function buildComparisonComment(applicants) {
155
+ const sorted = [...applicants].sort((a, b) => b.score - a.score);
156
+ let c = `## ๐Ÿค– Contributor Agent โ€” Applicant Comparison\n\n`;
157
+ c += `| Rank | Applicant | Tier | Score | Repo PRs | Languages | Activity |\n`;
158
+ c += `|------|-----------|------|-------|----------|-----------|----------|\n`;
159
+ sorted.forEach((a, i) => {
160
+ const medal = i === 0 ? '๐Ÿฅ‡' : i === 1 ? '๐Ÿฅˆ' : i === 2 ? '๐Ÿฅ‰' : `${i + 1}.`;
161
+ c += `| ${medal} | @${a.username} | ${TIER_EMOJI[a.tier]} ${a.tier} | **${a.score}**/100 | ${a.prsMerged} merged | ${a.relevantLanguages.slice(0, 3).join(', ') || '-'} | ${a.totalContributions} events |\n`;
162
+ });
163
+ if (sorted.length >= 2) {
164
+ const gap = sorted[0].score - sorted[1].score;
165
+ if (gap >= 20)
166
+ c += `\n> ๐Ÿ† **Clear winner:** @${sorted[0].username} leads by ${gap} points.\n`;
167
+ else if (gap >= 5)
168
+ c += `\n> ๐Ÿ† **Top pick:** @${sorted[0].username} leads by ${gap} points.\n`;
169
+ else
170
+ c += `\n> โš–๏ธ **Close match:** Top candidates within ${gap} points. Manual review recommended.\n`;
171
+ }
172
+ if (NOTIFY_USER)
173
+ c += `\n๐Ÿ”” cc @${NOTIFY_USER}\n`;
174
+ c += `\n---\n<sub>๐Ÿค– Contributor Agent ยท ${sorted.length} candidates ranked</sub>`;
175
+ return c;
176
+ }
177
+ // โ”€โ”€ Main โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
178
+ async function main() {
179
+ console.log(`\n๐Ÿค– Applicant Scorer โ€” ${OWNER}/${REPO} #${ISSUE_NUMBER}\n`);
180
+ if (!ISSUE_NUMBER) {
181
+ console.error('โŒ ISSUE_NUMBER required');
182
+ process.exit(1);
183
+ }
184
+ // Get trigger comment
185
+ let trigger;
186
+ if (COMMENT_ID) {
187
+ const { data: c } = await octokit.issues.getComment({ owner: OWNER, repo: REPO, comment_id: COMMENT_ID });
188
+ trigger = { body: c.body || '', user: c.user?.login || '' };
189
+ }
190
+ else {
191
+ const { data: cs } = await octokit.issues.listComments({ owner: OWNER, repo: REPO, issue_number: ISSUE_NUMBER, per_page: 1, direction: 'desc' });
192
+ if (!cs.length) {
193
+ console.log('No comments.');
194
+ return;
195
+ }
196
+ trigger = { body: cs[0].body || '', user: cs[0].user?.login || '' };
197
+ }
198
+ console.log(` Commenter: ${trigger.user}`);
199
+ if (!isApplicationComment(trigger.body)) {
200
+ console.log('\nโญ๏ธ Not an application โ€” skipping.');
201
+ return;
202
+ }
203
+ console.log('โœ… Application detected!\n');
204
+ // Get issue labels
205
+ const { data: issue } = await octokit.issues.get({ owner: OWNER, repo: REPO, issue_number: ISSUE_NUMBER });
206
+ const labels = issue.labels.map((l) => (typeof l === 'string' ? l : l.name || '')).filter(Boolean);
207
+ // Score current applicant
208
+ console.log(`๐Ÿ“Š Scoring @${trigger.user}...`);
209
+ const profile = await fetchProfile(trigger.user, trigger.body);
210
+ const scored = scoreApplicant(profile, labels);
211
+ console.log(` Score: ${scored.score}/100 (Tier ${scored.tier})\n`);
212
+ // Find all applicants
213
+ const { data: allComments } = await octokit.issues.listComments({ owner: OWNER, repo: REPO, issue_number: ISSUE_NUMBER, per_page: 100 });
214
+ const BOT_MARKER = '## ๐Ÿค– Contributor Agent';
215
+ const apps = allComments.filter((c) => c.user?.login !== 'github-actions[bot]' && !c.body?.includes(BOT_MARKER) && c.body && isApplicationComment(c.body));
216
+ const byUser = new Map();
217
+ apps.forEach((c) => { if (c.user?.login)
218
+ byUser.set(c.user.login, c); });
219
+ const unique = Array.from(byUser.values());
220
+ console.log(` ${unique.length} unique applicant(s)\n`);
221
+ // Score all
222
+ const allScored = [];
223
+ for (const c of unique) {
224
+ if (c.user?.login === trigger.user) {
225
+ allScored.push(scored);
226
+ continue;
227
+ }
228
+ console.log(` ๐Ÿ“Š Scoring @${c.user?.login}...`);
229
+ const p = await fetchProfile(c.user.login, c.body || '');
230
+ allScored.push(scoreApplicant(p, labels));
231
+ }
232
+ // Post individual review
233
+ const review = buildReviewComment(scored, unique.length);
234
+ const existingReview = allComments.find((c) => c.body?.includes(BOT_MARKER) && c.body?.includes(`@${trigger.user}`));
235
+ if (existingReview) {
236
+ await octokit.issues.updateComment({ owner: OWNER, repo: REPO, comment_id: existingReview.id, body: review });
237
+ }
238
+ else {
239
+ await octokit.issues.createComment({ owner: OWNER, repo: REPO, issue_number: ISSUE_NUMBER, body: review });
240
+ }
241
+ console.log(`โœ… Review posted for @${trigger.user}`);
242
+ // Post comparison if multiple
243
+ if (allScored.length >= 2) {
244
+ const comparison = buildComparisonComment(allScored);
245
+ const COMP_MARKER = '## ๐Ÿค– Contributor Agent โ€” Applicant Comparison';
246
+ const existingComp = allComments.find((c) => c.body?.includes(COMP_MARKER));
247
+ if (existingComp) {
248
+ await octokit.issues.updateComment({ owner: OWNER, repo: REPO, comment_id: existingComp.id, body: comparison });
249
+ }
250
+ else {
251
+ await octokit.issues.createComment({ owner: OWNER, repo: REPO, issue_number: ISSUE_NUMBER, body: comparison });
252
+ }
253
+ console.log(`โœ… Comparison posted (${allScored.length} candidates)\n`);
254
+ }
255
+ }
256
+ main().catch((e) => { console.error('Fatal:', e); process.exit(1); });