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 +4 -0
- package/dist/applicant-scorer.js +256 -0
- package/dist/cli.js +789 -0
- package/dist/commands/contribute.js +291 -0
- package/dist/commands/contributors.js +101 -0
- package/dist/commands/issues.js +319 -0
- package/dist/commands/prs.js +229 -0
- package/dist/commands/releases.js +53 -0
- package/dist/commands/repos.js +128 -0
- package/dist/core/github.js +106 -0
- package/dist/core/scorer.js +95 -0
- package/dist/create-issues.js +179 -0
- package/dist/pr-review.js +117 -0
- package/package.json +6 -3
- package/src/applicant-scorer.ts +1 -1
- package/src/cli.ts +345 -170
- package/src/commands/contribute.ts +331 -0
- package/src/commands/contributors.ts +1 -1
- package/src/commands/issues.ts +76 -9
- package/src/commands/prs.ts +1 -1
- package/src/commands/releases.ts +1 -1
- package/src/commands/repos.ts +41 -26
- package/src/core/github.ts +99 -15
- package/src/create-issues.ts +1 -1
- package/src/pr-review.ts +1 -1
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); });
|