terminalhire 0.4.1 → 0.4.4
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/dist/bin/jpi-bounties.js +193 -30
- package/dist/bin/jpi-claim.js +366 -0
- package/dist/bin/jpi-dispatch.js +679 -144
- package/dist/bin/jpi-jobs.js +186 -28
- package/dist/bin/jpi-learn.js +6 -0
- package/dist/bin/jpi-login.js +186 -28
- package/dist/bin/jpi-profile.js +6 -0
- package/dist/bin/jpi-refresh.js +186 -28
- package/dist/bin/jpi-save.js +6 -0
- package/dist/bin/jpi-sync.js +6 -0
- package/dist/src/claims.js +85 -0
- package/dist/src/profile.js +4 -0
- package/dist/src/signal.js +4 -0
- package/package.json +1 -1
package/dist/bin/jpi-bounties.js
CHANGED
|
@@ -1594,18 +1594,25 @@ var init_hn = __esm({
|
|
|
1594
1594
|
});
|
|
1595
1595
|
|
|
1596
1596
|
// ../../packages/core/src/feeds/bounty-gate.ts
|
|
1597
|
+
function isDenylistedRepo(fullName) {
|
|
1598
|
+
return DENYLIST_LC.has(fullName.toLowerCase());
|
|
1599
|
+
}
|
|
1600
|
+
function passesAntiFarm(amountUSD, stargazers) {
|
|
1601
|
+
return !(amountUSD > HIGH_VALUE_USD && stargazers < HIGH_VALUE_MIN_STARS);
|
|
1602
|
+
}
|
|
1597
1603
|
function ageDays(createdAtIso) {
|
|
1598
1604
|
const created = Date.parse(createdAtIso);
|
|
1599
1605
|
if (!Number.isFinite(created)) return 0;
|
|
1600
1606
|
return (Date.now() - created) / (1e3 * 60 * 60 * 24);
|
|
1601
1607
|
}
|
|
1602
1608
|
function passesMaturityGate(repo) {
|
|
1609
|
+
if (isDenylistedRepo(repo.fullName)) return false;
|
|
1603
1610
|
if (repo.archived || repo.disabled) return false;
|
|
1604
1611
|
if (repo.stargazers < MIN_REPO_STARS) return false;
|
|
1605
1612
|
if (ageDays(repo.createdAt) < MIN_REPO_AGE_DAYS) return false;
|
|
1606
1613
|
return true;
|
|
1607
1614
|
}
|
|
1608
|
-
var DEFAULT_BOUNTY_REPOS, MAX_BOUNTIES_PER_REPO, MIN_REPO_STARS, MIN_REPO_AGE_DAYS;
|
|
1615
|
+
var DEFAULT_BOUNTY_REPOS, BOUNTY_REPO_DENYLIST, DENYLIST_LC, MAX_BOUNTIES_PER_REPO, MAX_BOUNTIES_PER_DISCOVERED_REPO, MIN_REPO_STARS, HIGH_VALUE_USD, HIGH_VALUE_MIN_STARS, MIN_REPO_AGE_DAYS;
|
|
1609
1616
|
var init_bounty_gate = __esm({
|
|
1610
1617
|
"../../packages/core/src/feeds/bounty-gate.ts"() {
|
|
1611
1618
|
"use strict";
|
|
@@ -1622,8 +1629,13 @@ var init_bounty_gate = __esm({
|
|
|
1622
1629
|
"moorcheh-ai/memanto",
|
|
1623
1630
|
"PrismarineJS/mineflayer"
|
|
1624
1631
|
];
|
|
1632
|
+
BOUNTY_REPO_DENYLIST = ["SecureBananaLabs/bug-bounty"];
|
|
1633
|
+
DENYLIST_LC = new Set(BOUNTY_REPO_DENYLIST.map((r) => r.toLowerCase()));
|
|
1625
1634
|
MAX_BOUNTIES_PER_REPO = 10;
|
|
1635
|
+
MAX_BOUNTIES_PER_DISCOVERED_REPO = 3;
|
|
1626
1636
|
MIN_REPO_STARS = 5;
|
|
1637
|
+
HIGH_VALUE_USD = 500;
|
|
1638
|
+
HIGH_VALUE_MIN_STARS = 50;
|
|
1627
1639
|
MIN_REPO_AGE_DAYS = 30;
|
|
1628
1640
|
}
|
|
1629
1641
|
});
|
|
@@ -1776,6 +1788,54 @@ async function repoMetaCached(fullName) {
|
|
|
1776
1788
|
repoMetaCache.set(fullName, r);
|
|
1777
1789
|
return r;
|
|
1778
1790
|
}
|
|
1791
|
+
async function fetchRepoMeta2(fullName) {
|
|
1792
|
+
const repo = await repoMetaCached(fullName);
|
|
1793
|
+
if (!repo) return null;
|
|
1794
|
+
return {
|
|
1795
|
+
fullName: repo.full_name,
|
|
1796
|
+
stargazers: repo.stargazers_count,
|
|
1797
|
+
createdAt: repo.created_at,
|
|
1798
|
+
archived: repo.archived,
|
|
1799
|
+
disabled: repo.disabled
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
async function fetchRepoOpenPRRefs(fullName) {
|
|
1803
|
+
const hit = repoOpenPRRefsCache.get(fullName);
|
|
1804
|
+
if (hit !== void 0) return hit;
|
|
1805
|
+
const refs = /* @__PURE__ */ new Map();
|
|
1806
|
+
let scannedAny = false;
|
|
1807
|
+
for (let page = 1; page <= MAX_PR_PAGES; page++) {
|
|
1808
|
+
const prs = await ghJson(
|
|
1809
|
+
`/repos/${fullName}/pulls?state=open&per_page=100&page=${page}`
|
|
1810
|
+
);
|
|
1811
|
+
if (!Array.isArray(prs)) break;
|
|
1812
|
+
scannedAny = true;
|
|
1813
|
+
for (const pr of prs) {
|
|
1814
|
+
const counted = /* @__PURE__ */ new Set();
|
|
1815
|
+
for (const m of `${pr.title ?? ""}
|
|
1816
|
+
${pr.body ?? ""}`.matchAll(/#(\d+)\b/g)) {
|
|
1817
|
+
const n = Number(m[1]);
|
|
1818
|
+
if (!counted.has(n)) {
|
|
1819
|
+
counted.add(n);
|
|
1820
|
+
refs.set(n, (refs.get(n) ?? 0) + 1);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
if (prs.length < 100) break;
|
|
1825
|
+
}
|
|
1826
|
+
const result = scannedAny ? refs : null;
|
|
1827
|
+
repoOpenPRRefsCache.set(fullName, result);
|
|
1828
|
+
return result;
|
|
1829
|
+
}
|
|
1830
|
+
async function fetchIssueState(fullName, issueNumber) {
|
|
1831
|
+
const key = `${fullName}#${issueNumber}`;
|
|
1832
|
+
const hit = issueStateCache.get(key);
|
|
1833
|
+
if (hit !== void 0) return hit;
|
|
1834
|
+
const issue = await ghJson(`/repos/${fullName}/issues/${issueNumber}`);
|
|
1835
|
+
const state = issue?.state === "open" ? "open" : issue?.state === "closed" ? "closed" : null;
|
|
1836
|
+
issueStateCache.set(key, state);
|
|
1837
|
+
return state;
|
|
1838
|
+
}
|
|
1779
1839
|
async function fetchSearchBounties() {
|
|
1780
1840
|
const issues = (await searchBountyIssues()).slice(0, MAX_SEARCH_ISSUES_SCANNED);
|
|
1781
1841
|
const distinctRepos = [
|
|
@@ -1811,7 +1871,7 @@ async function fetchSearchBounties() {
|
|
|
1811
1871
|
amountUSD = await fetchCommentAmount(fullName, issue.number);
|
|
1812
1872
|
}
|
|
1813
1873
|
if (amountUSD == null) continue;
|
|
1814
|
-
if (amountUSD
|
|
1874
|
+
if (!passesAntiFarm(amountUSD, repo.stargazers_count)) continue;
|
|
1815
1875
|
const tags = normalize(
|
|
1816
1876
|
tokenize2([title, labels.join(" "), body.slice(0, 2e3)].join(" "))
|
|
1817
1877
|
);
|
|
@@ -1842,7 +1902,7 @@ async function fetchSearchBounties() {
|
|
|
1842
1902
|
}
|
|
1843
1903
|
return jobs;
|
|
1844
1904
|
}
|
|
1845
|
-
var GITHUB_API, BOUNTY_LABEL_RE, SEARCH_QUERIES, SEARCH_PER_PAGE, MAX_SEARCH_BOUNTIES, MAX_SEARCH_ISSUES_SCANNED, REPO_META_CONCURRENCY,
|
|
1905
|
+
var GITHUB_API, BOUNTY_LABEL_RE, SEARCH_QUERIES, SEARCH_PER_PAGE, MAX_SEARCH_BOUNTIES, MAX_SEARCH_ISSUES_SCANNED, REPO_META_CONCURRENCY, repoMetaCache, MAX_PR_PAGES, repoOpenPRRefsCache, issueStateCache, githubBounties;
|
|
1846
1906
|
var init_github_bounties = __esm({
|
|
1847
1907
|
"../../packages/core/src/feeds/github-bounties.ts"() {
|
|
1848
1908
|
"use strict";
|
|
@@ -1862,9 +1922,10 @@ var init_github_bounties = __esm({
|
|
|
1862
1922
|
MAX_SEARCH_BOUNTIES = 150;
|
|
1863
1923
|
MAX_SEARCH_ISSUES_SCANNED = 300;
|
|
1864
1924
|
REPO_META_CONCURRENCY = 15;
|
|
1865
|
-
SEARCH_HIGH_VALUE_USD = 500;
|
|
1866
|
-
SEARCH_HIGH_VALUE_MIN_STARS = 50;
|
|
1867
1925
|
repoMetaCache = /* @__PURE__ */ new Map();
|
|
1926
|
+
MAX_PR_PAGES = 3;
|
|
1927
|
+
repoOpenPRRefsCache = /* @__PURE__ */ new Map();
|
|
1928
|
+
issueStateCache = /* @__PURE__ */ new Map();
|
|
1868
1929
|
githubBounties = {
|
|
1869
1930
|
source: "bounty",
|
|
1870
1931
|
async fetch(opts) {
|
|
@@ -1915,16 +1976,23 @@ function repoFullNameFromUrl(url) {
|
|
|
1915
1976
|
const m = url?.match(/github\.com\/([^/]+)\/([^/]+)/i);
|
|
1916
1977
|
return m ? `${m[1]}/${m[2].replace(/\.git$/, "")}` : void 0;
|
|
1917
1978
|
}
|
|
1918
|
-
|
|
1979
|
+
function issueNumberFromUrl(url) {
|
|
1980
|
+
const m = url?.match(/\/issues\/(\d+)/);
|
|
1981
|
+
return m ? parseInt(m[1], 10) : void 0;
|
|
1982
|
+
}
|
|
1983
|
+
var OPIRE_REWARDS_URL, MIN_USD, MAX_USD, MAX_OPIRE_BOUNTIES, REPO_META_CONCURRENCY2, opire;
|
|
1919
1984
|
var init_opire = __esm({
|
|
1920
1985
|
"../../packages/core/src/feeds/opire.ts"() {
|
|
1921
1986
|
"use strict";
|
|
1922
1987
|
init_vocabulary();
|
|
1988
|
+
init_bounty_gate();
|
|
1989
|
+
init_github_bounties();
|
|
1923
1990
|
init_http();
|
|
1924
1991
|
OPIRE_REWARDS_URL = "https://api.opire.dev/rewards";
|
|
1925
1992
|
MIN_USD = 25;
|
|
1926
1993
|
MAX_USD = 25e3;
|
|
1927
1994
|
MAX_OPIRE_BOUNTIES = 100;
|
|
1995
|
+
REPO_META_CONCURRENCY2 = 15;
|
|
1928
1996
|
opire = {
|
|
1929
1997
|
source: "bounty",
|
|
1930
1998
|
async fetch() {
|
|
@@ -1943,7 +2011,7 @@ var init_opire = __esm({
|
|
|
1943
2011
|
console.warn("[opire] fetch failed \u2014", err);
|
|
1944
2012
|
return [];
|
|
1945
2013
|
}
|
|
1946
|
-
const
|
|
2014
|
+
const candidates = [];
|
|
1947
2015
|
for (const r of rewards) {
|
|
1948
2016
|
if (r.platform !== "GitHub") continue;
|
|
1949
2017
|
if (r.project && r.project.isPublic === false) continue;
|
|
@@ -1954,30 +2022,81 @@ var init_opire = __esm({
|
|
|
1954
2022
|
const title = (r.title ?? "").trim();
|
|
1955
2023
|
if (title.length < 4) continue;
|
|
1956
2024
|
const tags = normalize([...r.programmingLanguages ?? [], ...tokenize3(title)]);
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2025
|
+
const bounty = {
|
|
2026
|
+
amountUSD,
|
|
2027
|
+
estimatedEffort: effortFromAmount2(amountUSD),
|
|
2028
|
+
bountySource: "opire",
|
|
2029
|
+
claimUrl: r.url,
|
|
2030
|
+
repoFullName
|
|
2031
|
+
};
|
|
2032
|
+
candidates.push({
|
|
2033
|
+
repoFullName,
|
|
2034
|
+
amountUSD,
|
|
2035
|
+
issueNumber: issueNumberFromUrl(r.url),
|
|
2036
|
+
bounty,
|
|
2037
|
+
job: {
|
|
2038
|
+
id: `bounty:opire:${r.id}`,
|
|
2039
|
+
source: "bounty",
|
|
2040
|
+
title,
|
|
2041
|
+
company: r.organization?.name ?? repoFullName.split("/")[0],
|
|
2042
|
+
url: r.url,
|
|
2043
|
+
remote: true,
|
|
2044
|
+
location: "Remote",
|
|
2045
|
+
tags,
|
|
2046
|
+
roleType: "freelance",
|
|
2047
|
+
postedAt: Number.isFinite(r.createdAt) ? new Date(r.createdAt).toISOString() : void 0,
|
|
2048
|
+
applyMode: "direct",
|
|
2049
|
+
bounty,
|
|
2050
|
+
raw: r
|
|
2051
|
+
}
|
|
1977
2052
|
});
|
|
2053
|
+
}
|
|
2054
|
+
const distinctRepos = [...new Set(candidates.map((c) => c.repoFullName))];
|
|
2055
|
+
const meta = /* @__PURE__ */ new Map();
|
|
2056
|
+
for (let i = 0; i < distinctRepos.length; i += REPO_META_CONCURRENCY2) {
|
|
2057
|
+
const batch = distinctRepos.slice(i, i + REPO_META_CONCURRENCY2);
|
|
2058
|
+
const metas = await Promise.all(batch.map((name) => fetchRepoMeta2(name)));
|
|
2059
|
+
batch.forEach((name, k) => meta.set(name, metas[k]));
|
|
2060
|
+
}
|
|
2061
|
+
const gated = [];
|
|
2062
|
+
let dropped = 0;
|
|
2063
|
+
let ungated = 0;
|
|
2064
|
+
for (const c of candidates) {
|
|
2065
|
+
const m = meta.get(c.repoFullName);
|
|
2066
|
+
if (m) {
|
|
2067
|
+
if (!passesMaturityGate(m) || !passesAntiFarm(c.amountUSD, m.stargazers)) {
|
|
2068
|
+
dropped++;
|
|
2069
|
+
continue;
|
|
2070
|
+
}
|
|
2071
|
+
c.bounty.repoStars = m.stargazers;
|
|
2072
|
+
} else {
|
|
2073
|
+
ungated++;
|
|
2074
|
+
}
|
|
2075
|
+
gated.push(c);
|
|
2076
|
+
}
|
|
2077
|
+
const issueState = /* @__PURE__ */ new Map();
|
|
2078
|
+
for (let i = 0; i < gated.length; i += REPO_META_CONCURRENCY2) {
|
|
2079
|
+
const batch = gated.slice(i, i + REPO_META_CONCURRENCY2);
|
|
2080
|
+
const states = await Promise.all(
|
|
2081
|
+
batch.map(
|
|
2082
|
+
(c) => c.issueNumber != null ? fetchIssueState(c.repoFullName, c.issueNumber) : Promise.resolve(null)
|
|
2083
|
+
)
|
|
2084
|
+
);
|
|
2085
|
+
batch.forEach((c, k) => issueState.set(c.job.id, states[k]));
|
|
2086
|
+
}
|
|
2087
|
+
const jobs = [];
|
|
2088
|
+
let closed = 0;
|
|
2089
|
+
for (const c of gated) {
|
|
1978
2090
|
if (jobs.length >= MAX_OPIRE_BOUNTIES) break;
|
|
2091
|
+
if (issueState.get(c.job.id) === "closed") {
|
|
2092
|
+
closed++;
|
|
2093
|
+
continue;
|
|
2094
|
+
}
|
|
2095
|
+
jobs.push(c.job);
|
|
1979
2096
|
}
|
|
1980
|
-
console.info(
|
|
2097
|
+
console.info(
|
|
2098
|
+
`[opire] ${jobs.length} bounties (from ${rewards.length} rewards; ${dropped} repo-gated, ${closed} closed-issue, ${ungated} kept ungated)`
|
|
2099
|
+
);
|
|
1981
2100
|
return jobs;
|
|
1982
2101
|
}
|
|
1983
2102
|
};
|
|
@@ -2084,16 +2203,54 @@ async function aggregateBounties(opts) {
|
|
|
2084
2203
|
githubBounties.fetch({ slugs: opts?.repos }),
|
|
2085
2204
|
opire.fetch()
|
|
2086
2205
|
]);
|
|
2206
|
+
const allowlist = new Set(
|
|
2207
|
+
(opts?.repos && opts.repos.length > 0 ? opts.repos : DEFAULT_BOUNTY_REPOS).map(
|
|
2208
|
+
(r) => r.toLowerCase()
|
|
2209
|
+
)
|
|
2210
|
+
);
|
|
2087
2211
|
const seen = /* @__PURE__ */ new Set();
|
|
2212
|
+
const perRepo = /* @__PURE__ */ new Map();
|
|
2213
|
+
const seenRepoTitles = /* @__PURE__ */ new Set();
|
|
2088
2214
|
const out = [];
|
|
2089
2215
|
for (const j of [...gh, ...op]) {
|
|
2090
2216
|
const key = j.bounty?.claimUrl ?? j.url;
|
|
2091
2217
|
if (seen.has(key)) continue;
|
|
2218
|
+
const repo = j.bounty?.repoFullName?.toLowerCase();
|
|
2219
|
+
if (repo) {
|
|
2220
|
+
if (isDenylistedRepo(repo)) continue;
|
|
2221
|
+
const titleKey = `${repo} ${normalizeBountyTitle(j.title)}`;
|
|
2222
|
+
if (seenRepoTitles.has(titleKey)) continue;
|
|
2223
|
+
const cap = allowlist.has(repo) ? MAX_BOUNTIES_PER_REPO : MAX_BOUNTIES_PER_DISCOVERED_REPO;
|
|
2224
|
+
const n = perRepo.get(repo) ?? 0;
|
|
2225
|
+
if (n >= cap) continue;
|
|
2226
|
+
perRepo.set(repo, n + 1);
|
|
2227
|
+
seenRepoTitles.add(titleKey);
|
|
2228
|
+
}
|
|
2092
2229
|
seen.add(key);
|
|
2093
2230
|
out.push(j);
|
|
2094
2231
|
}
|
|
2232
|
+
const repos = [...new Set(out.map((j) => j.bounty?.repoFullName).filter((r) => !!r))];
|
|
2233
|
+
const refsByRepo = /* @__PURE__ */ new Map();
|
|
2234
|
+
const PR_REFS_CONCURRENCY = 15;
|
|
2235
|
+
for (let i = 0; i < repos.length; i += PR_REFS_CONCURRENCY) {
|
|
2236
|
+
const batch = repos.slice(i, i + PR_REFS_CONCURRENCY);
|
|
2237
|
+
const results = await Promise.all(batch.map((r) => fetchRepoOpenPRRefs(r)));
|
|
2238
|
+
batch.forEach((r, k) => refsByRepo.set(r, results[k]));
|
|
2239
|
+
}
|
|
2240
|
+
for (const j of out) {
|
|
2241
|
+
const num = bountyIssueNumber(j.bounty?.claimUrl);
|
|
2242
|
+
const refs = j.bounty?.repoFullName ? refsByRepo.get(j.bounty.repoFullName) : void 0;
|
|
2243
|
+
if (j.bounty && refs && num != null) j.bounty.competingOpenPRs = refs.get(num) ?? 0;
|
|
2244
|
+
}
|
|
2095
2245
|
return out;
|
|
2096
2246
|
}
|
|
2247
|
+
function bountyIssueNumber(url) {
|
|
2248
|
+
const m = url?.match(/\/issues\/(\d+)/);
|
|
2249
|
+
return m ? Number(m[1]) : void 0;
|
|
2250
|
+
}
|
|
2251
|
+
function normalizeBountyTitle(title) {
|
|
2252
|
+
return title.toLowerCase().replace(/#\d+\s*$/, "").replace(/[^a-z0-9]+/g, " ").trim();
|
|
2253
|
+
}
|
|
2097
2254
|
function flattenTiers(t) {
|
|
2098
2255
|
return [.../* @__PURE__ */ new Set([...t.bigco, ...t.scaleup, ...t.startup])];
|
|
2099
2256
|
}
|
|
@@ -2157,6 +2314,7 @@ var init_feeds = __esm({
|
|
|
2157
2314
|
init_opire();
|
|
2158
2315
|
init_workable();
|
|
2159
2316
|
init_bounty_gate();
|
|
2317
|
+
init_bounty_gate();
|
|
2160
2318
|
FEEDS = [greenhouse, ashby, lever, workable, himalayas, wwr, hn];
|
|
2161
2319
|
GREENHOUSE_SLUGS_BY_TIER = {
|
|
2162
2320
|
bigco: [
|
|
@@ -2678,6 +2836,7 @@ var limitArg = args.indexOf("--limit");
|
|
|
2678
2836
|
var LIMIT = limitArg !== -1 ? parseInt(args[limitArg + 1] ?? "15", 10) : DEFAULT_LIMIT;
|
|
2679
2837
|
var PRICED_ONLY = args.includes("--priced");
|
|
2680
2838
|
var SHOW_ALL = args.includes("--all");
|
|
2839
|
+
var WINNABLE_ONLY = args.includes("--winnable");
|
|
2681
2840
|
function readIndexCache() {
|
|
2682
2841
|
try {
|
|
2683
2842
|
const entry = JSON.parse(readFileSync3(INDEX_CACHE_FILE, "utf8"));
|
|
@@ -2724,9 +2883,11 @@ function printBounty(i, job, score, reason, matchedTags) {
|
|
|
2724
2883
|
const stars = b.repoStars != null ? ` \xB7 ${b.repoStars}\u2605` : "";
|
|
2725
2884
|
const effort = b.estimatedEffort ? ` \xB7 ${EFFORT_LABEL[b.estimatedEffort]}` : "";
|
|
2726
2885
|
const scoreStr = score > 0 ? ` \xB7 match ${Math.round(score * 100)}%` : "";
|
|
2886
|
+
const prs = b.competingOpenPRs;
|
|
2887
|
+
const contend = prs != null && prs > 0 ? ` \xB7 \u26A0 ${prs} PR${prs === 1 ? "" : "s"} in flight` : "";
|
|
2727
2888
|
console.log(`
|
|
2728
2889
|
${i + 1}. ${linkTitle(job.title, job.url)}`);
|
|
2729
|
-
console.log(` ${formatAmount(b)}${effort} \xB7 ${b.repoFullName ?? job.company}${stars}${scoreStr}`);
|
|
2890
|
+
console.log(` ${formatAmount(b)}${effort} \xB7 ${b.repoFullName ?? job.company}${stars}${scoreStr}${contend}`);
|
|
2730
2891
|
if (reason) console.log(` ${reason}`);
|
|
2731
2892
|
if (matchedTags && matchedTags.length) console.log(` Tags matched: ${matchedTags.slice(0, 5).join(", ")}`);
|
|
2732
2893
|
console.log(` id: ${job.id}`);
|
|
@@ -2738,6 +2899,7 @@ async function run() {
|
|
|
2738
2899
|
const index = await fetchIndex();
|
|
2739
2900
|
let bounties = (index.jobs ?? []).filter((j) => j.source === "bounty");
|
|
2740
2901
|
if (PRICED_ONLY) bounties = bounties.filter((j) => j.bounty?.amountUSD != null);
|
|
2902
|
+
if (WINNABLE_ONLY) bounties = bounties.filter((j) => (j.bounty?.competingOpenPRs ?? 0) === 0);
|
|
2741
2903
|
if (bounties.length === 0) {
|
|
2742
2904
|
console.log("\nNo bounties available right now. Try again later \u2014 supply refreshes through the day.");
|
|
2743
2905
|
return;
|
|
@@ -2757,7 +2919,8 @@ async function run() {
|
|
|
2757
2919
|
}
|
|
2758
2920
|
const score = (j) => ranked.get(j.id)?.score ?? 0;
|
|
2759
2921
|
const amt = (j) => j.bounty?.amountUSD ?? -1;
|
|
2760
|
-
|
|
2922
|
+
const contested = (j) => (j.bounty?.competingOpenPRs ?? 0) > 0 ? 1 : 0;
|
|
2923
|
+
bounties.sort((a, b) => contested(a) - contested(b) || score(b) - score(a) || amt(b) - amt(a));
|
|
2761
2924
|
const shown = SHOW_ALL ? bounties : bounties.slice(0, LIMIT);
|
|
2762
2925
|
const matchedCount = bounties.filter((j) => score(j) > 0).length;
|
|
2763
2926
|
console.log(
|