terminalhire 0.4.0 → 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 +636 -42
- package/dist/bin/jpi-claim.js +366 -0
- package/dist/bin/jpi-dispatch.js +1124 -158
- package/dist/bin/jpi-jobs.js +629 -40
- package/dist/bin/jpi-learn.js +51 -1
- package/dist/bin/jpi-login.js +629 -40
- package/dist/bin/jpi-profile.js +51 -1
- package/dist/bin/jpi-refresh.js +631 -42
- package/dist/bin/jpi-save.js +51 -1
- package/dist/bin/jpi-spinner.js +1 -1
- package/dist/bin/jpi-sync.js +51 -1
- package/dist/bin/spinner.js +2 -2
- package/dist/src/claims.js +85 -0
- package/dist/src/profile.js +16 -1
- package/dist/src/signal.js +16 -1
- package/package.json +1 -1
package/dist/bin/jpi-bounties.js
CHANGED
|
@@ -148,7 +148,7 @@ var init_graph_data = __esm({
|
|
|
148
148
|
{ id: "airflow", parents: ["data-engineering"], synonyms: ["apache-airflow"] },
|
|
149
149
|
{ id: "dbt", parents: ["data-engineering"] },
|
|
150
150
|
{ id: "ml", synonyms: ["machine-learning"], related: [{ to: "pytorch", w: 0.5 }, { to: "tensorflow", w: 0.5 }, { to: "scikit-learn", w: 0.5 }, { to: "data-engineering", w: 0.4 }] },
|
|
151
|
-
{ id: "llm", parents: ["ml"], synonyms: ["llms", "genai", "generative-ai"], related: [{ to: "langchain", w: 0.5 }, { to: "rag", w: 0.55 }, { to: "openai", w: 0.45 }, { to: "anthropic", w: 0.45 }] },
|
|
151
|
+
{ id: "llm", parents: ["ml"], synonyms: ["llms", "genai", "generative-ai", "gpt"], related: [{ to: "langchain", w: 0.5 }, { to: "rag", w: 0.55 }, { to: "openai", w: 0.45 }, { to: "anthropic", w: 0.45 }] },
|
|
152
152
|
{ id: "pytorch", parents: ["ml"], synonyms: ["torch"], related: [{ to: "tensorflow", w: 0.5 }] },
|
|
153
153
|
{ id: "tensorflow", parents: ["ml"], synonyms: ["keras", "tf-keras"] },
|
|
154
154
|
{ id: "pandas", parents: ["python"], related: [{ to: "numpy", w: 0.6 }, { to: "data-engineering", w: 0.45 }, { to: "spark", w: 0.4 }] },
|
|
@@ -161,6 +161,14 @@ var init_graph_data = __esm({
|
|
|
161
161
|
{ id: "anthropic", parents: ["llm"], synonyms: ["claude"] },
|
|
162
162
|
{ id: "rag", parents: ["llm"], synonyms: ["retrieval-augmented-generation"] },
|
|
163
163
|
{ id: "mlops", parents: ["ml"], related: [{ to: "devops", w: 0.4 }] },
|
|
164
|
+
{ id: "agents", parents: ["llm"], synonyms: ["agentic", "ai-agents", "multi-agent"], related: [{ to: "rag", w: 0.4 }] },
|
|
165
|
+
{ id: "mcp", parents: ["agents"], synonyms: ["model-context-protocol"], related: [{ to: "llm", w: 0.45 }] },
|
|
166
|
+
{ id: "inference", parents: ["ml"], synonyms: ["model-inference", "llm-inference", "model-serving"], related: [{ to: "mlops", w: 0.5 }, { to: "llm", w: 0.4 }] },
|
|
167
|
+
{ id: "embeddings", parents: ["ml"], synonyms: ["embedding", "vector-embeddings"], related: [{ to: "rag", w: 0.55 }, { to: "llm", w: 0.45 }] },
|
|
168
|
+
{ id: "prompt-engineering", parents: ["llm"], synonyms: ["prompting", "prompt"] },
|
|
169
|
+
{ id: "fine-tuning", parents: ["ml"], synonyms: ["finetuning", "fine-tune", "rlhf"], related: [{ to: "llm", w: 0.5 }] },
|
|
170
|
+
{ id: "computer-vision", parents: ["ml"], synonyms: ["image-recognition", "object-detection"] },
|
|
171
|
+
{ id: "recsys", parents: ["ml"], synonyms: ["recommender-systems", "recommendation-systems", "recommendation"] },
|
|
164
172
|
// ── Mobile ──────────────────────────────────────────────────────────────────
|
|
165
173
|
{ id: "mobile", related: [{ to: "ios", w: 0.5 }, { to: "android", w: 0.5 }] },
|
|
166
174
|
{ id: "ios", parents: ["mobile", "swift"], related: [{ to: "android", w: 0.4 }] },
|
|
@@ -583,7 +591,10 @@ var init_vocabulary = __esm({
|
|
|
583
591
|
function ghHeaders(token) {
|
|
584
592
|
const headers = {
|
|
585
593
|
Accept: "application/vnd.github+json",
|
|
586
|
-
"X-GitHub-Api-Version": "2022-11-28"
|
|
594
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
595
|
+
// GitHub's REST API REQUIRES a User-Agent; serverless runtimes don't always
|
|
596
|
+
// send a default (omitting it yields a 403 "administrative rules" error).
|
|
597
|
+
"User-Agent": "terminalhire"
|
|
587
598
|
};
|
|
588
599
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
589
600
|
return headers;
|
|
@@ -727,16 +738,25 @@ async function fetchRepoMeta(owner, name, token, cache) {
|
|
|
727
738
|
cache.set(key, meta);
|
|
728
739
|
return meta;
|
|
729
740
|
}
|
|
730
|
-
|
|
741
|
+
function emptyCredential(status) {
|
|
742
|
+
return { status, byDomain: {}, qualifyingTotal: 0, computedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
743
|
+
}
|
|
744
|
+
async function fetchPublicOrgs(login, token) {
|
|
745
|
+
try {
|
|
746
|
+
const orgs = await ghFetch(
|
|
747
|
+
`/users/${login}/orgs?per_page=100`,
|
|
748
|
+
token
|
|
749
|
+
);
|
|
750
|
+
return new Set(orgs.map((o) => o.login.toLowerCase()));
|
|
751
|
+
} catch {
|
|
752
|
+
return /* @__PURE__ */ new Set();
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
async function computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates = {
|
|
756
|
+
minStars: MIN_STARS,
|
|
757
|
+
minContributors: MIN_CONTRIBUTORS
|
|
758
|
+
}) {
|
|
731
759
|
const computedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
732
|
-
const empty = (status) => ({
|
|
733
|
-
status,
|
|
734
|
-
byDomain: {},
|
|
735
|
-
qualifyingTotal: 0,
|
|
736
|
-
computedAt
|
|
737
|
-
});
|
|
738
|
-
if (!token) return empty("no-token");
|
|
739
|
-
const ownedOrgs = await fetchOwnedOrgs(token);
|
|
740
760
|
const loginLc = login.toLowerCase();
|
|
741
761
|
let items;
|
|
742
762
|
try {
|
|
@@ -747,8 +767,9 @@ async function computeAcceptanceCredential(login, token, cache = /* @__PURE__ */
|
|
|
747
767
|
);
|
|
748
768
|
items = res.items ?? [];
|
|
749
769
|
} catch (err) {
|
|
750
|
-
const msg = String(err);
|
|
751
|
-
|
|
770
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
771
|
+
console.warn("[acceptance] search failed:", msg);
|
|
772
|
+
return emptyCredential(/HTTP 403|HTTP 429|rate limit/i.test(msg) ? "rate-limited" : "failed");
|
|
752
773
|
}
|
|
753
774
|
const byDomain = {};
|
|
754
775
|
let qualifyingTotal = 0;
|
|
@@ -762,8 +783,8 @@ async function computeAcceptanceCredential(login, token, cache = /* @__PURE__ */
|
|
|
762
783
|
const meta = await fetchRepoMeta(repo.owner, repo.name, token, cache);
|
|
763
784
|
if (!meta) continue;
|
|
764
785
|
if (meta.archived || meta.fork) continue;
|
|
765
|
-
if (meta.stars <
|
|
766
|
-
if (meta.contributors !== void 0 && meta.contributors <
|
|
786
|
+
if (meta.stars < gates.minStars) continue;
|
|
787
|
+
if (meta.contributors !== void 0 && meta.contributors < gates.minContributors) continue;
|
|
767
788
|
qualifyingTotal += 1;
|
|
768
789
|
const mergedAt = item.pull_request?.merged_at ?? item.closed_at ?? item.created_at;
|
|
769
790
|
const rawDomains = [meta.language ?? "", ...meta.topics].filter(Boolean);
|
|
@@ -784,6 +805,18 @@ async function computeAcceptanceCredential(login, token, cache = /* @__PURE__ */
|
|
|
784
805
|
}
|
|
785
806
|
return { status: "ok", byDomain: finalDomains, qualifyingTotal, computedAt };
|
|
786
807
|
}
|
|
808
|
+
async function computeAcceptanceCredential(login, token, cache = /* @__PURE__ */ new Map()) {
|
|
809
|
+
if (!token) return emptyCredential("no-token");
|
|
810
|
+
const ownedOrgs = await fetchOwnedOrgs(token);
|
|
811
|
+
return computeAcceptanceFromSearch(login, token, ownedOrgs, cache);
|
|
812
|
+
}
|
|
813
|
+
async function computeAcceptanceCredentialPublic(login, token, cache = /* @__PURE__ */ new Map(), opts) {
|
|
814
|
+
if (!token) return emptyCredential("no-token");
|
|
815
|
+
const ownedOrgs = await fetchPublicOrgs(login, token);
|
|
816
|
+
for (const org of opts?.includeOrgs ?? []) ownedOrgs.delete(org.toLowerCase());
|
|
817
|
+
const gates = opts?.relaxGates ? { minStars: 0, minContributors: 0 } : void 0;
|
|
818
|
+
return computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates);
|
|
819
|
+
}
|
|
787
820
|
function acceptanceCountForDomains(cred, domains) {
|
|
788
821
|
if (cred.status !== "ok") return 0;
|
|
789
822
|
let max = 0;
|
|
@@ -802,7 +835,60 @@ function bestAcceptanceDomain(cred, domains) {
|
|
|
802
835
|
}
|
|
803
836
|
return best;
|
|
804
837
|
}
|
|
805
|
-
|
|
838
|
+
function resumeRecencyDecay(lastSeenIso, now) {
|
|
839
|
+
const ageMs = now - new Date(lastSeenIso).getTime();
|
|
840
|
+
if (!Number.isFinite(ageMs)) return 0;
|
|
841
|
+
return Math.pow(0.5, ageMs / RESUME_DECAY_HALF_LIFE_MS);
|
|
842
|
+
}
|
|
843
|
+
async function fetchRepoRecency(login, token) {
|
|
844
|
+
try {
|
|
845
|
+
const repos = await ghFetch(`/users/${login}/repos?sort=pushed&per_page=100`, token);
|
|
846
|
+
return repos.filter((r) => !r.fork && !!r.pushed_at).map((r) => ({ pushedAt: r.pushed_at, language: r.language ?? null, topics: r.topics ?? [] }));
|
|
847
|
+
} catch {
|
|
848
|
+
return [];
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function deriveResumeTrend(cred, repoRecency, now = Date.now()) {
|
|
852
|
+
const agg = /* @__PURE__ */ new Map();
|
|
853
|
+
const bump = (domain, when, count, mergedPRs) => {
|
|
854
|
+
const e = agg.get(domain);
|
|
855
|
+
if (!e) {
|
|
856
|
+
agg.set(domain, { count, last: when, earliest: when, mergedPRs });
|
|
857
|
+
} else {
|
|
858
|
+
e.count += count;
|
|
859
|
+
e.mergedPRs += mergedPRs;
|
|
860
|
+
if (when > e.last) e.last = when;
|
|
861
|
+
if (when < e.earliest) e.earliest = when;
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
if (cred.status === "ok") {
|
|
865
|
+
for (const [domain, d] of Object.entries(cred.byDomain)) {
|
|
866
|
+
bump(domain, d.lastMergedAt, d.mergedPRs, d.mergedPRs);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
for (const r of repoRecency) {
|
|
870
|
+
for (const domain of new Set(normalize([r.language ?? "", ...r.topics].filter(Boolean)))) {
|
|
871
|
+
bump(domain, r.pushedAt, 1, 0);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
const oneHalfLifeAgoIso = new Date(now - RESUME_DECAY_HALF_LIFE_MS).toISOString();
|
|
875
|
+
const scored = [];
|
|
876
|
+
for (const [domain, e] of agg.entries()) {
|
|
877
|
+
const recencyScore2 = resumeRecencyDecay(e.last, now);
|
|
878
|
+
const weight = e.count * recencyScore2;
|
|
879
|
+
if (weight < RESUME_MIN_SCORE) continue;
|
|
880
|
+
let direction;
|
|
881
|
+
if (e.earliest > oneHalfLifeAgoIso) direction = "new";
|
|
882
|
+
else if (recencyScore2 >= 0.5) direction = "up";
|
|
883
|
+
else direction = "down";
|
|
884
|
+
scored.push({
|
|
885
|
+
t: { domain, direction, recencyScore: Math.round(recencyScore2 * 1e3) / 1e3, mergedPRs: e.mergedPRs },
|
|
886
|
+
weight
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
return scored.sort((a, b) => b.weight - a.weight).slice(0, 12).map((s) => s.t);
|
|
890
|
+
}
|
|
891
|
+
var MIN_STARS, MIN_CONTRIBUTORS, CANDIDATE_PR_PAGE, TRIVIAL_PR_TITLE, RESUME_DECAY_HALF_LIFE_MS, RESUME_MIN_SCORE;
|
|
806
892
|
var init_github = __esm({
|
|
807
893
|
"../../packages/core/src/github.ts"() {
|
|
808
894
|
"use strict";
|
|
@@ -811,6 +897,8 @@ var init_github = __esm({
|
|
|
811
897
|
MIN_CONTRIBUTORS = 10;
|
|
812
898
|
CANDIDATE_PR_PAGE = 50;
|
|
813
899
|
TRIVIAL_PR_TITLE = /^\s*(fix\s+typo|typo\b|update\s+readme|readme\b|docs?:|docs?\(|chore:|chore\(|style:|ci:|build:|bump\b|update\s+dependenc)/i;
|
|
900
|
+
RESUME_DECAY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
901
|
+
RESUME_MIN_SCORE = 0.05;
|
|
814
902
|
}
|
|
815
903
|
});
|
|
816
904
|
|
|
@@ -958,6 +1046,18 @@ var init_matcher = __esm({
|
|
|
958
1046
|
}
|
|
959
1047
|
});
|
|
960
1048
|
|
|
1049
|
+
// ../../packages/core/src/feeds/http.ts
|
|
1050
|
+
function fetchWithTimeout(input, init, timeoutMs = FEED_FETCH_TIMEOUT_MS) {
|
|
1051
|
+
return fetch(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
1052
|
+
}
|
|
1053
|
+
var FEED_FETCH_TIMEOUT_MS;
|
|
1054
|
+
var init_http = __esm({
|
|
1055
|
+
"../../packages/core/src/feeds/http.ts"() {
|
|
1056
|
+
"use strict";
|
|
1057
|
+
FEED_FETCH_TIMEOUT_MS = 1e4;
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
|
|
961
1061
|
// ../../packages/core/src/feeds/greenhouse.ts
|
|
962
1062
|
function extractTags(job) {
|
|
963
1063
|
const body = [
|
|
@@ -976,7 +1076,7 @@ async function fetchSlug(slug) {
|
|
|
976
1076
|
const url = `https://boards-api.greenhouse.io/v1/boards/${slug}/jobs?content=true`;
|
|
977
1077
|
let res;
|
|
978
1078
|
try {
|
|
979
|
-
res = await
|
|
1079
|
+
res = await fetchWithTimeout(url, { headers: { Accept: "application/json" } });
|
|
980
1080
|
} catch (err) {
|
|
981
1081
|
console.warn(`[greenhouse] ${slug}: network error \u2014`, err);
|
|
982
1082
|
return [];
|
|
@@ -1018,6 +1118,7 @@ var init_greenhouse = __esm({
|
|
|
1018
1118
|
"../../packages/core/src/feeds/greenhouse.ts"() {
|
|
1019
1119
|
"use strict";
|
|
1020
1120
|
init_vocabulary();
|
|
1121
|
+
init_http();
|
|
1021
1122
|
FALLBACK_SLUGS = [
|
|
1022
1123
|
"stripe",
|
|
1023
1124
|
"linear",
|
|
@@ -1088,7 +1189,7 @@ function inferRemote2(job) {
|
|
|
1088
1189
|
}
|
|
1089
1190
|
async function fetchSlug2(slug) {
|
|
1090
1191
|
const url = `https://api.ashbyhq.com/posting-api/job-board/${slug}`;
|
|
1091
|
-
const res = await
|
|
1192
|
+
const res = await fetchWithTimeout(url, {
|
|
1092
1193
|
headers: { Accept: "application/json" }
|
|
1093
1194
|
});
|
|
1094
1195
|
if (!res.ok) {
|
|
@@ -1120,6 +1221,7 @@ var init_ashby = __esm({
|
|
|
1120
1221
|
"../../packages/core/src/feeds/ashby.ts"() {
|
|
1121
1222
|
"use strict";
|
|
1122
1223
|
init_vocabulary();
|
|
1224
|
+
init_http();
|
|
1123
1225
|
ashby = {
|
|
1124
1226
|
source: "ashby",
|
|
1125
1227
|
async fetch(opts) {
|
|
@@ -1170,7 +1272,7 @@ function toIso(ms) {
|
|
|
1170
1272
|
}
|
|
1171
1273
|
async function fetchSlug3(slug) {
|
|
1172
1274
|
const url = `https://api.lever.co/v0/postings/${slug}?mode=json`;
|
|
1173
|
-
const res = await
|
|
1275
|
+
const res = await fetchWithTimeout(url, { headers: { Accept: "application/json" } });
|
|
1174
1276
|
if (!res.ok) {
|
|
1175
1277
|
throw new Error(`Lever ${slug}: HTTP ${res.status}`);
|
|
1176
1278
|
}
|
|
@@ -1201,6 +1303,7 @@ var init_lever = __esm({
|
|
|
1201
1303
|
"../../packages/core/src/feeds/lever.ts"() {
|
|
1202
1304
|
"use strict";
|
|
1203
1305
|
init_vocabulary();
|
|
1306
|
+
init_http();
|
|
1204
1307
|
lever = {
|
|
1205
1308
|
source: "lever",
|
|
1206
1309
|
async fetch(opts) {
|
|
@@ -1242,12 +1345,13 @@ var init_himalayas = __esm({
|
|
|
1242
1345
|
"../../packages/core/src/feeds/himalayas.ts"() {
|
|
1243
1346
|
"use strict";
|
|
1244
1347
|
init_vocabulary();
|
|
1348
|
+
init_http();
|
|
1245
1349
|
himalayas = {
|
|
1246
1350
|
source: "himalayas",
|
|
1247
1351
|
async fetch(opts) {
|
|
1248
1352
|
const limit = opts?.limit ?? 100;
|
|
1249
1353
|
const url = `https://himalayas.app/jobs/api?limit=${limit}`;
|
|
1250
|
-
const res = await
|
|
1354
|
+
const res = await fetchWithTimeout(url, {
|
|
1251
1355
|
headers: { Accept: "application/json" }
|
|
1252
1356
|
});
|
|
1253
1357
|
if (!res.ok) {
|
|
@@ -1356,12 +1460,13 @@ var init_wwr = __esm({
|
|
|
1356
1460
|
"use strict";
|
|
1357
1461
|
init_vocabulary();
|
|
1358
1462
|
init_entities();
|
|
1463
|
+
init_http();
|
|
1359
1464
|
WWR_RSS_URL = "https://weworkremotely.com/remote-jobs.rss";
|
|
1360
1465
|
wwr = {
|
|
1361
1466
|
source: "wwr",
|
|
1362
1467
|
async fetch(opts) {
|
|
1363
1468
|
const limit = opts?.limit ?? 200;
|
|
1364
|
-
const res = await
|
|
1469
|
+
const res = await fetchWithTimeout(WWR_RSS_URL, {
|
|
1365
1470
|
headers: { Accept: "application/rss+xml, application/xml, text/xml" }
|
|
1366
1471
|
});
|
|
1367
1472
|
if (!res.ok) {
|
|
@@ -1369,6 +1474,11 @@ var init_wwr = __esm({
|
|
|
1369
1474
|
}
|
|
1370
1475
|
const xml = await res.text();
|
|
1371
1476
|
const items = parseRss(xml).slice(0, limit);
|
|
1477
|
+
function safeIso(s) {
|
|
1478
|
+
if (!s) return void 0;
|
|
1479
|
+
const d = new Date(s);
|
|
1480
|
+
return Number.isNaN(d.getTime()) ? void 0 : d.toISOString();
|
|
1481
|
+
}
|
|
1372
1482
|
return items.map((item) => ({
|
|
1373
1483
|
id: extractId(item.link),
|
|
1374
1484
|
source: "wwr",
|
|
@@ -1380,7 +1490,7 @@ var init_wwr = __esm({
|
|
|
1380
1490
|
location: "Remote",
|
|
1381
1491
|
tags: extractTags5(item),
|
|
1382
1492
|
roleType: inferRoleType(item.category),
|
|
1383
|
-
postedAt:
|
|
1493
|
+
postedAt: safeIso(item.pubDate),
|
|
1384
1494
|
applyMode: "direct",
|
|
1385
1495
|
raw: item
|
|
1386
1496
|
}));
|
|
@@ -1446,13 +1556,14 @@ var init_hn = __esm({
|
|
|
1446
1556
|
"use strict";
|
|
1447
1557
|
init_vocabulary();
|
|
1448
1558
|
init_entities();
|
|
1559
|
+
init_http();
|
|
1449
1560
|
ALGOLIA_SEARCH = "https://hn.algolia.com/api/v1/search?query=Ask+HN%3A+Who+is+Hiring%3F&tags=story,ask_hn&hitsPerPage=1";
|
|
1450
1561
|
ALGOLIA_ITEMS = "https://hn.algolia.com/api/v1/items/";
|
|
1451
1562
|
hn = {
|
|
1452
1563
|
source: "hn",
|
|
1453
1564
|
async fetch(opts) {
|
|
1454
1565
|
const limit = opts?.limit ?? 150;
|
|
1455
|
-
const searchRes = await
|
|
1566
|
+
const searchRes = await fetchWithTimeout(ALGOLIA_SEARCH, {
|
|
1456
1567
|
headers: { Accept: "application/json" }
|
|
1457
1568
|
});
|
|
1458
1569
|
if (!searchRes.ok) {
|
|
@@ -1463,7 +1574,7 @@ var init_hn = __esm({
|
|
|
1463
1574
|
if (!story) {
|
|
1464
1575
|
throw new Error('HN: No "Who is Hiring" story found');
|
|
1465
1576
|
}
|
|
1466
|
-
const itemRes = await
|
|
1577
|
+
const itemRes = await fetchWithTimeout(`${ALGOLIA_ITEMS}${story.objectID}`, {
|
|
1467
1578
|
headers: { Accept: "application/json" }
|
|
1468
1579
|
});
|
|
1469
1580
|
if (!itemRes.ok) {
|
|
@@ -1483,18 +1594,25 @@ var init_hn = __esm({
|
|
|
1483
1594
|
});
|
|
1484
1595
|
|
|
1485
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
|
+
}
|
|
1486
1603
|
function ageDays(createdAtIso) {
|
|
1487
1604
|
const created = Date.parse(createdAtIso);
|
|
1488
1605
|
if (!Number.isFinite(created)) return 0;
|
|
1489
1606
|
return (Date.now() - created) / (1e3 * 60 * 60 * 24);
|
|
1490
1607
|
}
|
|
1491
1608
|
function passesMaturityGate(repo) {
|
|
1609
|
+
if (isDenylistedRepo(repo.fullName)) return false;
|
|
1492
1610
|
if (repo.archived || repo.disabled) return false;
|
|
1493
1611
|
if (repo.stargazers < MIN_REPO_STARS) return false;
|
|
1494
1612
|
if (ageDays(repo.createdAt) < MIN_REPO_AGE_DAYS) return false;
|
|
1495
1613
|
return true;
|
|
1496
1614
|
}
|
|
1497
|
-
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;
|
|
1498
1616
|
var init_bounty_gate = __esm({
|
|
1499
1617
|
"../../packages/core/src/feeds/bounty-gate.ts"() {
|
|
1500
1618
|
"use strict";
|
|
@@ -1511,8 +1629,13 @@ var init_bounty_gate = __esm({
|
|
|
1511
1629
|
"moorcheh-ai/memanto",
|
|
1512
1630
|
"PrismarineJS/mineflayer"
|
|
1513
1631
|
];
|
|
1632
|
+
BOUNTY_REPO_DENYLIST = ["SecureBananaLabs/bug-bounty"];
|
|
1633
|
+
DENYLIST_LC = new Set(BOUNTY_REPO_DENYLIST.map((r) => r.toLowerCase()));
|
|
1514
1634
|
MAX_BOUNTIES_PER_REPO = 10;
|
|
1635
|
+
MAX_BOUNTIES_PER_DISCOVERED_REPO = 3;
|
|
1515
1636
|
MIN_REPO_STARS = 5;
|
|
1637
|
+
HIGH_VALUE_USD = 500;
|
|
1638
|
+
HIGH_VALUE_MIN_STARS = 50;
|
|
1516
1639
|
MIN_REPO_AGE_DAYS = 30;
|
|
1517
1640
|
}
|
|
1518
1641
|
});
|
|
@@ -1557,7 +1680,7 @@ function isBountyIssue(issue) {
|
|
|
1557
1680
|
async function ghJson(path) {
|
|
1558
1681
|
let res;
|
|
1559
1682
|
try {
|
|
1560
|
-
res = await
|
|
1683
|
+
res = await fetchWithTimeout(`${GITHUB_API}${path}`, { headers: authHeaders() });
|
|
1561
1684
|
} catch (err) {
|
|
1562
1685
|
console.warn(`[github-bounties] network error ${path} \u2014`, err);
|
|
1563
1686
|
return null;
|
|
@@ -1639,31 +1762,435 @@ async function fetchRepoBounties(repoFullName) {
|
|
|
1639
1762
|
};
|
|
1640
1763
|
}));
|
|
1641
1764
|
}
|
|
1642
|
-
|
|
1765
|
+
function repoFullNameFromApiUrl(url) {
|
|
1766
|
+
const m = url.match(/\/repos\/([^/]+)\/([^/]+)\/?$/);
|
|
1767
|
+
return m ? `${m[1]}/${m[2]}` : null;
|
|
1768
|
+
}
|
|
1769
|
+
async function searchBountyIssues() {
|
|
1770
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
1771
|
+
for (const q of SEARCH_QUERIES) {
|
|
1772
|
+
const res = await ghJson(
|
|
1773
|
+
`/search/issues?q=${encodeURIComponent(q)}&sort=created&order=desc&per_page=${SEARCH_PER_PAGE}`
|
|
1774
|
+
);
|
|
1775
|
+
for (const it of res?.items ?? []) {
|
|
1776
|
+
if (it.pull_request) continue;
|
|
1777
|
+
if (!byUrl.has(it.html_url)) byUrl.set(it.html_url, it);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
return [...byUrl.values()].sort(
|
|
1781
|
+
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
async function repoMetaCached(fullName) {
|
|
1785
|
+
const hit = repoMetaCache.get(fullName);
|
|
1786
|
+
if (hit !== void 0) return hit;
|
|
1787
|
+
const r = await ghJson(`/repos/${fullName}`) ?? null;
|
|
1788
|
+
repoMetaCache.set(fullName, r);
|
|
1789
|
+
return r;
|
|
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
|
+
}
|
|
1839
|
+
async function fetchSearchBounties() {
|
|
1840
|
+
const issues = (await searchBountyIssues()).slice(0, MAX_SEARCH_ISSUES_SCANNED);
|
|
1841
|
+
const distinctRepos = [
|
|
1842
|
+
...new Set(
|
|
1843
|
+
issues.map((i) => repoFullNameFromApiUrl(i.repository_url)).filter((x) => !!x)
|
|
1844
|
+
)
|
|
1845
|
+
];
|
|
1846
|
+
for (let i = 0; i < distinctRepos.length; i += REPO_META_CONCURRENCY) {
|
|
1847
|
+
await Promise.all(distinctRepos.slice(i, i + REPO_META_CONCURRENCY).map(repoMetaCached));
|
|
1848
|
+
}
|
|
1849
|
+
const jobs = [];
|
|
1850
|
+
const perRepo = /* @__PURE__ */ new Map();
|
|
1851
|
+
for (const issue of issues) {
|
|
1852
|
+
if (jobs.length >= MAX_SEARCH_BOUNTIES) break;
|
|
1853
|
+
const fullName = repoFullNameFromApiUrl(issue.repository_url);
|
|
1854
|
+
if (!fullName) continue;
|
|
1855
|
+
if ((perRepo.get(fullName) ?? 0) >= MAX_BOUNTIES_PER_REPO) continue;
|
|
1856
|
+
const repo = await repoMetaCached(fullName);
|
|
1857
|
+
if (!repo) continue;
|
|
1858
|
+
const passes = passesMaturityGate({
|
|
1859
|
+
fullName: repo.full_name,
|
|
1860
|
+
stargazers: repo.stargazers_count,
|
|
1861
|
+
createdAt: repo.created_at,
|
|
1862
|
+
archived: repo.archived,
|
|
1863
|
+
disabled: repo.disabled
|
|
1864
|
+
});
|
|
1865
|
+
if (!passes) continue;
|
|
1866
|
+
const title = decodeEntities(issue.title).trim();
|
|
1867
|
+
const body = issue.body ? decodeEntities(issue.body) : "";
|
|
1868
|
+
const labels = labelNames(issue);
|
|
1869
|
+
let amountUSD = parseAmountUSD(title) ?? parseAmountUSD(labels.join(" ")) ?? parseAmountUSD(body);
|
|
1870
|
+
if (amountUSD == null && labels.some((n) => /💎|💰/.test(n))) {
|
|
1871
|
+
amountUSD = await fetchCommentAmount(fullName, issue.number);
|
|
1872
|
+
}
|
|
1873
|
+
if (amountUSD == null) continue;
|
|
1874
|
+
if (!passesAntiFarm(amountUSD, repo.stargazers_count)) continue;
|
|
1875
|
+
const tags = normalize(
|
|
1876
|
+
tokenize2([title, labels.join(" "), body.slice(0, 2e3)].join(" "))
|
|
1877
|
+
);
|
|
1878
|
+
perRepo.set(fullName, (perRepo.get(fullName) ?? 0) + 1);
|
|
1879
|
+
jobs.push({
|
|
1880
|
+
id: `bounty:${fullName}#${issue.number}`,
|
|
1881
|
+
source: "bounty",
|
|
1882
|
+
title,
|
|
1883
|
+
company: repo.owner.login,
|
|
1884
|
+
url: issue.html_url,
|
|
1885
|
+
remote: true,
|
|
1886
|
+
location: "Remote",
|
|
1887
|
+
tags,
|
|
1888
|
+
roleType: "freelance",
|
|
1889
|
+
postedAt: issue.created_at,
|
|
1890
|
+
applyMode: "direct",
|
|
1891
|
+
bounty: {
|
|
1892
|
+
amountUSD,
|
|
1893
|
+
estimatedEffort: effortFromAmount(amountUSD),
|
|
1894
|
+
bountySource: "github",
|
|
1895
|
+
claimUrl: issue.html_url,
|
|
1896
|
+
repoFullName: fullName,
|
|
1897
|
+
repoStars: repo.stargazers_count,
|
|
1898
|
+
issueBody: body.slice(0, 1e3) || void 0
|
|
1899
|
+
},
|
|
1900
|
+
raw: issue
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
return jobs;
|
|
1904
|
+
}
|
|
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;
|
|
1643
1906
|
var init_github_bounties = __esm({
|
|
1644
1907
|
"../../packages/core/src/feeds/github-bounties.ts"() {
|
|
1645
1908
|
"use strict";
|
|
1646
1909
|
init_vocabulary();
|
|
1647
1910
|
init_entities();
|
|
1648
1911
|
init_bounty_gate();
|
|
1912
|
+
init_http();
|
|
1649
1913
|
GITHUB_API = "https://api.github.com";
|
|
1650
1914
|
BOUNTY_LABEL_RE = /bounty|reward|funded|💎|💰/i;
|
|
1915
|
+
SEARCH_QUERIES = [
|
|
1916
|
+
'label:"\u{1F48E} Bounty" type:issue state:open',
|
|
1917
|
+
// Algora-applied — highest signal
|
|
1918
|
+
"label:bounty type:issue state:open",
|
|
1919
|
+
'label:"\u{1F4B0} Bounty" type:issue state:open'
|
|
1920
|
+
];
|
|
1921
|
+
SEARCH_PER_PAGE = 100;
|
|
1922
|
+
MAX_SEARCH_BOUNTIES = 150;
|
|
1923
|
+
MAX_SEARCH_ISSUES_SCANNED = 300;
|
|
1924
|
+
REPO_META_CONCURRENCY = 15;
|
|
1925
|
+
repoMetaCache = /* @__PURE__ */ new Map();
|
|
1926
|
+
MAX_PR_PAGES = 3;
|
|
1927
|
+
repoOpenPRRefsCache = /* @__PURE__ */ new Map();
|
|
1928
|
+
issueStateCache = /* @__PURE__ */ new Map();
|
|
1651
1929
|
githubBounties = {
|
|
1652
1930
|
source: "bounty",
|
|
1653
1931
|
async fetch(opts) {
|
|
1654
|
-
const
|
|
1655
|
-
|
|
1656
|
-
|
|
1932
|
+
const allowlist = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : DEFAULT_BOUNTY_REPOS;
|
|
1933
|
+
const [searched, listed] = await Promise.all([
|
|
1934
|
+
fetchSearchBounties().catch((e) => {
|
|
1935
|
+
console.warn("[github-bounties] search discovery failed:", e);
|
|
1936
|
+
return [];
|
|
1937
|
+
}),
|
|
1938
|
+
Promise.allSettled(allowlist.map(fetchRepoBounties)).then(
|
|
1939
|
+
(settled) => settled.flatMap((r) => r.status === "fulfilled" ? r.value : [])
|
|
1940
|
+
)
|
|
1941
|
+
]);
|
|
1942
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1943
|
+
const out = [];
|
|
1944
|
+
for (const j of [...searched, ...listed]) {
|
|
1945
|
+
if (!seen.has(j.id)) {
|
|
1946
|
+
seen.add(j.id);
|
|
1947
|
+
out.push(j);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
console.info(
|
|
1951
|
+
`[github-bounties] total: ${out.length} bounties (${searched.length} search + ${listed.length} allowlist, deduped)`
|
|
1952
|
+
);
|
|
1953
|
+
return out;
|
|
1954
|
+
}
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1959
|
+
// ../../packages/core/src/feeds/opire.ts
|
|
1960
|
+
function tokenize3(text) {
|
|
1961
|
+
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter((w) => w.length > 1);
|
|
1962
|
+
}
|
|
1963
|
+
function effortFromAmount2(usd) {
|
|
1964
|
+
if (usd == null) return void 0;
|
|
1965
|
+
if (usd < 150) return "small";
|
|
1966
|
+
if (usd < 750) return "medium";
|
|
1967
|
+
return "large";
|
|
1968
|
+
}
|
|
1969
|
+
function priceToUSD(p) {
|
|
1970
|
+
if (!p || typeof p.value !== "number") return void 0;
|
|
1971
|
+
if (p.unit === "USD_CENT") return Math.round(p.value) / 100;
|
|
1972
|
+
if (p.unit === "USD") return p.value;
|
|
1973
|
+
return void 0;
|
|
1974
|
+
}
|
|
1975
|
+
function repoFullNameFromUrl(url) {
|
|
1976
|
+
const m = url?.match(/github\.com\/([^/]+)\/([^/]+)/i);
|
|
1977
|
+
return m ? `${m[1]}/${m[2].replace(/\.git$/, "")}` : void 0;
|
|
1978
|
+
}
|
|
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;
|
|
1984
|
+
var init_opire = __esm({
|
|
1985
|
+
"../../packages/core/src/feeds/opire.ts"() {
|
|
1986
|
+
"use strict";
|
|
1987
|
+
init_vocabulary();
|
|
1988
|
+
init_bounty_gate();
|
|
1989
|
+
init_github_bounties();
|
|
1990
|
+
init_http();
|
|
1991
|
+
OPIRE_REWARDS_URL = "https://api.opire.dev/rewards";
|
|
1992
|
+
MIN_USD = 25;
|
|
1993
|
+
MAX_USD = 25e3;
|
|
1994
|
+
MAX_OPIRE_BOUNTIES = 100;
|
|
1995
|
+
REPO_META_CONCURRENCY2 = 15;
|
|
1996
|
+
opire = {
|
|
1997
|
+
source: "bounty",
|
|
1998
|
+
async fetch() {
|
|
1999
|
+
let rewards;
|
|
2000
|
+
try {
|
|
2001
|
+
const res = await fetchWithTimeout(OPIRE_REWARDS_URL, {
|
|
2002
|
+
headers: { Accept: "application/json", "User-Agent": "terminalhire" }
|
|
2003
|
+
});
|
|
2004
|
+
if (!res.ok) {
|
|
2005
|
+
console.warn(`[opire] HTTP ${res.status}`);
|
|
2006
|
+
return [];
|
|
2007
|
+
}
|
|
2008
|
+
const json = await res.json();
|
|
2009
|
+
rewards = Array.isArray(json) ? json : json?.data ?? json?.items ?? [];
|
|
2010
|
+
} catch (err) {
|
|
2011
|
+
console.warn("[opire] fetch failed \u2014", err);
|
|
2012
|
+
return [];
|
|
2013
|
+
}
|
|
2014
|
+
const candidates = [];
|
|
2015
|
+
for (const r of rewards) {
|
|
2016
|
+
if (r.platform !== "GitHub") continue;
|
|
2017
|
+
if (r.project && r.project.isPublic === false) continue;
|
|
2018
|
+
const repoFullName = repoFullNameFromUrl(r.project?.url ?? r.url);
|
|
2019
|
+
if (!repoFullName) continue;
|
|
2020
|
+
const amountUSD = priceToUSD(r.pendingPrice);
|
|
2021
|
+
if (amountUSD == null || amountUSD < MIN_USD || amountUSD > MAX_USD) continue;
|
|
2022
|
+
const title = (r.title ?? "").trim();
|
|
2023
|
+
if (title.length < 4) continue;
|
|
2024
|
+
const tags = normalize([...r.programmingLanguages ?? [], ...tokenize3(title)]);
|
|
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
|
+
}
|
|
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) {
|
|
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);
|
|
2096
|
+
}
|
|
2097
|
+
console.info(
|
|
2098
|
+
`[opire] ${jobs.length} bounties (from ${rewards.length} rewards; ${dropped} repo-gated, ${closed} closed-issue, ${ungated} kept ungated)`
|
|
2099
|
+
);
|
|
2100
|
+
return jobs;
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
// ../../packages/core/src/feeds/workable.ts
|
|
2107
|
+
function locationStr(loc) {
|
|
2108
|
+
if (!loc) return "";
|
|
2109
|
+
return [loc.city, loc.country].filter(Boolean).join(", ");
|
|
2110
|
+
}
|
|
2111
|
+
function isRemote(j) {
|
|
2112
|
+
return j.remote === true || (j.workplace ?? "").toLowerCase() === "remote";
|
|
2113
|
+
}
|
|
2114
|
+
function extractTags7(j) {
|
|
2115
|
+
const body = [...j.department ?? [], locationStr(j.location)].filter(Boolean).join(" ");
|
|
2116
|
+
return extractSkillTags(j.title, body);
|
|
2117
|
+
}
|
|
2118
|
+
async function fetchAccount(account) {
|
|
2119
|
+
const url = `https://apply.workable.com/api/v3/accounts/${account}/jobs`;
|
|
2120
|
+
const out = [];
|
|
2121
|
+
let token;
|
|
2122
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
2123
|
+
let res;
|
|
2124
|
+
try {
|
|
2125
|
+
res = await fetchWithTimeout(url, {
|
|
2126
|
+
method: "POST",
|
|
2127
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
2128
|
+
body: JSON.stringify(token ? { token } : {})
|
|
2129
|
+
});
|
|
2130
|
+
} catch (err) {
|
|
2131
|
+
console.warn(`[workable] ${account}: network error \u2014`, err);
|
|
2132
|
+
break;
|
|
2133
|
+
}
|
|
2134
|
+
if (!res.ok) {
|
|
2135
|
+
console.warn(`[workable] ${account}: HTTP ${res.status}`);
|
|
2136
|
+
break;
|
|
2137
|
+
}
|
|
2138
|
+
let data;
|
|
2139
|
+
try {
|
|
2140
|
+
data = await res.json();
|
|
2141
|
+
} catch (err) {
|
|
2142
|
+
console.warn(`[workable] ${account}: JSON parse error \u2014`, err);
|
|
2143
|
+
break;
|
|
2144
|
+
}
|
|
2145
|
+
const results = data.results ?? [];
|
|
2146
|
+
for (const j of results) {
|
|
2147
|
+
if (j.state && j.state !== "published") continue;
|
|
2148
|
+
out.push({
|
|
2149
|
+
id: `workable:${j.id}`,
|
|
2150
|
+
source: "workable",
|
|
2151
|
+
title: j.title,
|
|
2152
|
+
company: account,
|
|
2153
|
+
url: `https://apply.workable.com/${account}/j/${j.shortcode}/`,
|
|
2154
|
+
remote: isRemote(j),
|
|
2155
|
+
location: locationStr(j.location) || void 0,
|
|
2156
|
+
tags: extractTags7(j),
|
|
2157
|
+
roleType: "full_time",
|
|
2158
|
+
postedAt: j.published,
|
|
2159
|
+
applyMode: "direct",
|
|
2160
|
+
raw: j
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
token = data.token;
|
|
2164
|
+
if (!token || results.length === 0) break;
|
|
2165
|
+
}
|
|
2166
|
+
if (out.length > 0) console.info(`[workable] ${account}: ${out.length} jobs`);
|
|
2167
|
+
return out;
|
|
2168
|
+
}
|
|
2169
|
+
var FALLBACK_ACCOUNTS, MAX_PAGES, workable;
|
|
2170
|
+
var init_workable = __esm({
|
|
2171
|
+
"../../packages/core/src/feeds/workable.ts"() {
|
|
2172
|
+
"use strict";
|
|
2173
|
+
init_vocabulary();
|
|
2174
|
+
init_http();
|
|
2175
|
+
FALLBACK_ACCOUNTS = ["zego", "workmotion"];
|
|
2176
|
+
MAX_PAGES = 5;
|
|
2177
|
+
workable = {
|
|
2178
|
+
source: "workable",
|
|
2179
|
+
async fetch(opts) {
|
|
2180
|
+
const accounts = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : FALLBACK_ACCOUNTS;
|
|
2181
|
+
console.info(`[workable] fetching ${accounts.length} accounts: ${accounts.join(", ")}`);
|
|
2182
|
+
const results = await Promise.allSettled(accounts.map(fetchAccount));
|
|
1657
2183
|
const jobs = [];
|
|
1658
2184
|
let failures = 0;
|
|
1659
|
-
for (const r of
|
|
1660
|
-
if (r.status === "fulfilled")
|
|
1661
|
-
|
|
2185
|
+
for (const r of results) {
|
|
2186
|
+
if (r.status === "fulfilled") {
|
|
2187
|
+
jobs.push(...r.value);
|
|
2188
|
+
} else {
|
|
1662
2189
|
failures++;
|
|
1663
|
-
console.warn("[
|
|
2190
|
+
console.warn("[workable] account fetch rejected:", r.reason);
|
|
1664
2191
|
}
|
|
1665
2192
|
}
|
|
1666
|
-
console.info(`[
|
|
2193
|
+
console.info(`[workable] total: ${jobs.length} jobs, ${failures} account failures`);
|
|
1667
2194
|
return jobs;
|
|
1668
2195
|
}
|
|
1669
2196
|
};
|
|
@@ -1672,7 +2199,57 @@ var init_github_bounties = __esm({
|
|
|
1672
2199
|
|
|
1673
2200
|
// ../../packages/core/src/feeds/index.ts
|
|
1674
2201
|
async function aggregateBounties(opts) {
|
|
1675
|
-
|
|
2202
|
+
const [gh, op] = await Promise.all([
|
|
2203
|
+
githubBounties.fetch({ slugs: opts?.repos }),
|
|
2204
|
+
opire.fetch()
|
|
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
|
+
);
|
|
2211
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2212
|
+
const perRepo = /* @__PURE__ */ new Map();
|
|
2213
|
+
const seenRepoTitles = /* @__PURE__ */ new Set();
|
|
2214
|
+
const out = [];
|
|
2215
|
+
for (const j of [...gh, ...op]) {
|
|
2216
|
+
const key = j.bounty?.claimUrl ?? j.url;
|
|
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
|
+
}
|
|
2229
|
+
seen.add(key);
|
|
2230
|
+
out.push(j);
|
|
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
|
+
}
|
|
2245
|
+
return out;
|
|
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();
|
|
1676
2253
|
}
|
|
1677
2254
|
function flattenTiers(t) {
|
|
1678
2255
|
return [.../* @__PURE__ */ new Set([...t.bigco, ...t.scaleup, ...t.startup])];
|
|
@@ -1681,18 +2258,20 @@ async function aggregate(opts) {
|
|
|
1681
2258
|
const ghSlugs = opts?.slugs?.["greenhouse"] ?? DEFAULT_GREENHOUSE_SLUGS;
|
|
1682
2259
|
const ashbySlugs = opts?.slugs?.["ashby"] ?? DEFAULT_ASHBY_SLUGS;
|
|
1683
2260
|
const leverSlugs = opts?.slugs?.["lever"] ?? DEFAULT_LEVER_SLUGS;
|
|
2261
|
+
const workableSlugs = opts?.slugs?.["workable"] ?? DEFAULT_WORKABLE_SLUGS;
|
|
1684
2262
|
const limit = opts?.limit ?? 150;
|
|
1685
2263
|
const settled = await Promise.allSettled([
|
|
1686
2264
|
greenhouse.fetch({ slugs: ghSlugs, limit }),
|
|
1687
2265
|
ashby.fetch({ slugs: ashbySlugs, limit }),
|
|
1688
2266
|
lever.fetch({ slugs: leverSlugs, limit }),
|
|
2267
|
+
workable.fetch({ slugs: workableSlugs, limit }),
|
|
1689
2268
|
himalayas.fetch({ limit }),
|
|
1690
2269
|
wwr.fetch({ limit }),
|
|
1691
2270
|
hn.fetch({ limit })
|
|
1692
2271
|
]);
|
|
1693
2272
|
const seen = /* @__PURE__ */ new Set();
|
|
1694
2273
|
const jobs = [];
|
|
1695
|
-
const sourceNames = ["greenhouse", "ashby", "lever", "himalayas", "wwr", "hn"];
|
|
2274
|
+
const sourceNames = ["greenhouse", "ashby", "lever", "workable", "himalayas", "wwr", "hn"];
|
|
1696
2275
|
for (let i = 0; i < settled.length; i++) {
|
|
1697
2276
|
const result = settled[i];
|
|
1698
2277
|
if (result.status === "rejected") {
|
|
@@ -1708,7 +2287,7 @@ async function aggregate(opts) {
|
|
|
1708
2287
|
}
|
|
1709
2288
|
if (opts?.includeBounties !== false) {
|
|
1710
2289
|
try {
|
|
1711
|
-
const bounties = await
|
|
2290
|
+
const bounties = await aggregateBounties({ repos: opts?.slugs?.["bounty"] });
|
|
1712
2291
|
for (const b of bounties) {
|
|
1713
2292
|
if (!seen.has(b.id)) {
|
|
1714
2293
|
seen.add(b.id);
|
|
@@ -1721,7 +2300,7 @@ async function aggregate(opts) {
|
|
|
1721
2300
|
}
|
|
1722
2301
|
return jobs;
|
|
1723
2302
|
}
|
|
1724
|
-
var FEEDS, GREENHOUSE_SLUGS_BY_TIER, ASHBY_SLUGS_BY_TIER, LEVER_SLUGS_BY_TIER, DEFAULT_GREENHOUSE_SLUGS, DEFAULT_ASHBY_SLUGS, DEFAULT_LEVER_SLUGS;
|
|
2303
|
+
var FEEDS, GREENHOUSE_SLUGS_BY_TIER, ASHBY_SLUGS_BY_TIER, LEVER_SLUGS_BY_TIER, DEFAULT_GREENHOUSE_SLUGS, DEFAULT_ASHBY_SLUGS, DEFAULT_LEVER_SLUGS, DEFAULT_WORKABLE_SLUGS;
|
|
1725
2304
|
var init_feeds = __esm({
|
|
1726
2305
|
"../../packages/core/src/feeds/index.ts"() {
|
|
1727
2306
|
"use strict";
|
|
@@ -1732,8 +2311,11 @@ var init_feeds = __esm({
|
|
|
1732
2311
|
init_wwr();
|
|
1733
2312
|
init_hn();
|
|
1734
2313
|
init_github_bounties();
|
|
2314
|
+
init_opire();
|
|
2315
|
+
init_workable();
|
|
2316
|
+
init_bounty_gate();
|
|
1735
2317
|
init_bounty_gate();
|
|
1736
|
-
FEEDS = [greenhouse, ashby, lever, himalayas, wwr, hn];
|
|
2318
|
+
FEEDS = [greenhouse, ashby, lever, workable, himalayas, wwr, hn];
|
|
1737
2319
|
GREENHOUSE_SLUGS_BY_TIER = {
|
|
1738
2320
|
bigco: [
|
|
1739
2321
|
"stripe",
|
|
@@ -1839,6 +2421,7 @@ var init_feeds = __esm({
|
|
|
1839
2421
|
DEFAULT_GREENHOUSE_SLUGS = flattenTiers(GREENHOUSE_SLUGS_BY_TIER);
|
|
1840
2422
|
DEFAULT_ASHBY_SLUGS = flattenTiers(ASHBY_SLUGS_BY_TIER);
|
|
1841
2423
|
DEFAULT_LEVER_SLUGS = flattenTiers(LEVER_SLUGS_BY_TIER);
|
|
2424
|
+
DEFAULT_WORKABLE_SLUGS = ["zego", "workmotion"];
|
|
1842
2425
|
}
|
|
1843
2426
|
});
|
|
1844
2427
|
|
|
@@ -1939,6 +2522,7 @@ __export(src_exports, {
|
|
|
1939
2522
|
DEFAULT_BOUNTY_REPOS: () => DEFAULT_BOUNTY_REPOS,
|
|
1940
2523
|
DEFAULT_GREENHOUSE_SLUGS: () => DEFAULT_GREENHOUSE_SLUGS,
|
|
1941
2524
|
DEFAULT_LEVER_SLUGS: () => DEFAULT_LEVER_SLUGS,
|
|
2525
|
+
DEFAULT_WORKABLE_SLUGS: () => DEFAULT_WORKABLE_SLUGS,
|
|
1942
2526
|
EXAMPLE_BUYER: () => EXAMPLE_BUYER,
|
|
1943
2527
|
FEEDS: () => FEEDS,
|
|
1944
2528
|
GRAPH: () => GRAPH,
|
|
@@ -1957,10 +2541,13 @@ __export(src_exports, {
|
|
|
1957
2541
|
buildIndex: () => buildIndex,
|
|
1958
2542
|
buildReason: () => buildReason,
|
|
1959
2543
|
computeAcceptanceCredential: () => computeAcceptanceCredential,
|
|
2544
|
+
computeAcceptanceCredentialPublic: () => computeAcceptanceCredentialPublic,
|
|
1960
2545
|
coreTagsFromTitle: () => coreTagsFromTitle,
|
|
2546
|
+
deriveResumeTrend: () => deriveResumeTrend,
|
|
1961
2547
|
expandWeighted: () => expandWeighted,
|
|
1962
2548
|
extractSkillTags: () => extractSkillTags,
|
|
1963
2549
|
fetchGitHubProfile: () => fetchGitHubProfile,
|
|
2550
|
+
fetchRepoRecency: () => fetchRepoRecency,
|
|
1964
2551
|
flattenTiers: () => flattenTiers,
|
|
1965
2552
|
getBuyer: () => getBuyer,
|
|
1966
2553
|
githubBounties: () => githubBounties,
|
|
@@ -1974,9 +2561,11 @@ __export(src_exports, {
|
|
|
1974
2561
|
looksLikeEngRole: () => looksLikeEngRole,
|
|
1975
2562
|
match: () => match,
|
|
1976
2563
|
normalize: () => normalize,
|
|
2564
|
+
opire: () => opire,
|
|
1977
2565
|
passesMaturityGate: () => passesMaturityGate,
|
|
1978
2566
|
tokenize: () => tokenize,
|
|
1979
2567
|
validateGraph: () => validateGraph,
|
|
2568
|
+
workable: () => workable,
|
|
1980
2569
|
wwr: () => wwr
|
|
1981
2570
|
});
|
|
1982
2571
|
var init_src = __esm({
|
|
@@ -2247,6 +2836,7 @@ var limitArg = args.indexOf("--limit");
|
|
|
2247
2836
|
var LIMIT = limitArg !== -1 ? parseInt(args[limitArg + 1] ?? "15", 10) : DEFAULT_LIMIT;
|
|
2248
2837
|
var PRICED_ONLY = args.includes("--priced");
|
|
2249
2838
|
var SHOW_ALL = args.includes("--all");
|
|
2839
|
+
var WINNABLE_ONLY = args.includes("--winnable");
|
|
2250
2840
|
function readIndexCache() {
|
|
2251
2841
|
try {
|
|
2252
2842
|
const entry = JSON.parse(readFileSync3(INDEX_CACHE_FILE, "utf8"));
|
|
@@ -2293,9 +2883,11 @@ function printBounty(i, job, score, reason, matchedTags) {
|
|
|
2293
2883
|
const stars = b.repoStars != null ? ` \xB7 ${b.repoStars}\u2605` : "";
|
|
2294
2884
|
const effort = b.estimatedEffort ? ` \xB7 ${EFFORT_LABEL[b.estimatedEffort]}` : "";
|
|
2295
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` : "";
|
|
2296
2888
|
console.log(`
|
|
2297
2889
|
${i + 1}. ${linkTitle(job.title, job.url)}`);
|
|
2298
|
-
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}`);
|
|
2299
2891
|
if (reason) console.log(` ${reason}`);
|
|
2300
2892
|
if (matchedTags && matchedTags.length) console.log(` Tags matched: ${matchedTags.slice(0, 5).join(", ")}`);
|
|
2301
2893
|
console.log(` id: ${job.id}`);
|
|
@@ -2307,6 +2899,7 @@ async function run() {
|
|
|
2307
2899
|
const index = await fetchIndex();
|
|
2308
2900
|
let bounties = (index.jobs ?? []).filter((j) => j.source === "bounty");
|
|
2309
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);
|
|
2310
2903
|
if (bounties.length === 0) {
|
|
2311
2904
|
console.log("\nNo bounties available right now. Try again later \u2014 supply refreshes through the day.");
|
|
2312
2905
|
return;
|
|
@@ -2326,7 +2919,8 @@ async function run() {
|
|
|
2326
2919
|
}
|
|
2327
2920
|
const score = (j) => ranked.get(j.id)?.score ?? 0;
|
|
2328
2921
|
const amt = (j) => j.bounty?.amountUSD ?? -1;
|
|
2329
|
-
|
|
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));
|
|
2330
2924
|
const shown = SHOW_ALL ? bounties : bounties.slice(0, LIMIT);
|
|
2331
2925
|
const matchedCount = bounties.filter((j) => score(j) > 0).length;
|
|
2332
2926
|
console.log(
|