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.
@@ -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 > SEARCH_HIGH_VALUE_USD && repo.stargazers_count < SEARCH_HIGH_VALUE_MIN_STARS) continue;
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, SEARCH_HIGH_VALUE_USD, SEARCH_HIGH_VALUE_MIN_STARS, repoMetaCache, githubBounties;
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
- var OPIRE_REWARDS_URL, MIN_USD, MAX_USD, MAX_OPIRE_BOUNTIES, opire;
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 jobs = [];
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
- jobs.push({
1958
- id: `bounty:opire:${r.id}`,
1959
- source: "bounty",
1960
- title,
1961
- company: r.organization?.name ?? repoFullName.split("/")[0],
1962
- url: r.url,
1963
- remote: true,
1964
- location: "Remote",
1965
- tags,
1966
- roleType: "freelance",
1967
- postedAt: Number.isFinite(r.createdAt) ? new Date(r.createdAt).toISOString() : void 0,
1968
- applyMode: "direct",
1969
- bounty: {
1970
- amountUSD,
1971
- estimatedEffort: effortFromAmount2(amountUSD),
1972
- bountySource: "opire",
1973
- claimUrl: r.url,
1974
- repoFullName
1975
- },
1976
- raw: r
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(`[opire] ${jobs.length} bounties (from ${rewards.length} rewards)`);
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
- bounties.sort((a, b) => score(b) - score(a) || amt(b) - amt(a));
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(