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-dispatch.js
CHANGED
|
@@ -362,7 +362,7 @@ var init_graph_data = __esm({
|
|
|
362
362
|
{ id: "airflow", parents: ["data-engineering"], synonyms: ["apache-airflow"] },
|
|
363
363
|
{ id: "dbt", parents: ["data-engineering"] },
|
|
364
364
|
{ 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 }] },
|
|
365
|
-
{ 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 }] },
|
|
365
|
+
{ 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 }] },
|
|
366
366
|
{ id: "pytorch", parents: ["ml"], synonyms: ["torch"], related: [{ to: "tensorflow", w: 0.5 }] },
|
|
367
367
|
{ id: "tensorflow", parents: ["ml"], synonyms: ["keras", "tf-keras"] },
|
|
368
368
|
{ id: "pandas", parents: ["python"], related: [{ to: "numpy", w: 0.6 }, { to: "data-engineering", w: 0.45 }, { to: "spark", w: 0.4 }] },
|
|
@@ -375,6 +375,14 @@ var init_graph_data = __esm({
|
|
|
375
375
|
{ id: "anthropic", parents: ["llm"], synonyms: ["claude"] },
|
|
376
376
|
{ id: "rag", parents: ["llm"], synonyms: ["retrieval-augmented-generation"] },
|
|
377
377
|
{ id: "mlops", parents: ["ml"], related: [{ to: "devops", w: 0.4 }] },
|
|
378
|
+
{ id: "agents", parents: ["llm"], synonyms: ["agentic", "ai-agents", "multi-agent"], related: [{ to: "rag", w: 0.4 }] },
|
|
379
|
+
{ id: "mcp", parents: ["agents"], synonyms: ["model-context-protocol"], related: [{ to: "llm", w: 0.45 }] },
|
|
380
|
+
{ id: "inference", parents: ["ml"], synonyms: ["model-inference", "llm-inference", "model-serving"], related: [{ to: "mlops", w: 0.5 }, { to: "llm", w: 0.4 }] },
|
|
381
|
+
{ id: "embeddings", parents: ["ml"], synonyms: ["embedding", "vector-embeddings"], related: [{ to: "rag", w: 0.55 }, { to: "llm", w: 0.45 }] },
|
|
382
|
+
{ id: "prompt-engineering", parents: ["llm"], synonyms: ["prompting", "prompt"] },
|
|
383
|
+
{ id: "fine-tuning", parents: ["ml"], synonyms: ["finetuning", "fine-tune", "rlhf"], related: [{ to: "llm", w: 0.5 }] },
|
|
384
|
+
{ id: "computer-vision", parents: ["ml"], synonyms: ["image-recognition", "object-detection"] },
|
|
385
|
+
{ id: "recsys", parents: ["ml"], synonyms: ["recommender-systems", "recommendation-systems", "recommendation"] },
|
|
378
386
|
// ── Mobile ──────────────────────────────────────────────────────────────────
|
|
379
387
|
{ id: "mobile", related: [{ to: "ios", w: 0.5 }, { to: "android", w: 0.5 }] },
|
|
380
388
|
{ id: "ios", parents: ["mobile", "swift"], related: [{ to: "android", w: 0.4 }] },
|
|
@@ -797,7 +805,10 @@ var init_vocabulary = __esm({
|
|
|
797
805
|
function ghHeaders(token) {
|
|
798
806
|
const headers = {
|
|
799
807
|
Accept: "application/vnd.github+json",
|
|
800
|
-
"X-GitHub-Api-Version": "2022-11-28"
|
|
808
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
809
|
+
// GitHub's REST API REQUIRES a User-Agent; serverless runtimes don't always
|
|
810
|
+
// send a default (omitting it yields a 403 "administrative rules" error).
|
|
811
|
+
"User-Agent": "terminalhire"
|
|
801
812
|
};
|
|
802
813
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
803
814
|
return headers;
|
|
@@ -941,16 +952,25 @@ async function fetchRepoMeta(owner, name, token, cache) {
|
|
|
941
952
|
cache.set(key, meta);
|
|
942
953
|
return meta;
|
|
943
954
|
}
|
|
944
|
-
|
|
955
|
+
function emptyCredential(status) {
|
|
956
|
+
return { status, byDomain: {}, qualifyingTotal: 0, computedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
957
|
+
}
|
|
958
|
+
async function fetchPublicOrgs(login, token) {
|
|
959
|
+
try {
|
|
960
|
+
const orgs = await ghFetch(
|
|
961
|
+
`/users/${login}/orgs?per_page=100`,
|
|
962
|
+
token
|
|
963
|
+
);
|
|
964
|
+
return new Set(orgs.map((o) => o.login.toLowerCase()));
|
|
965
|
+
} catch {
|
|
966
|
+
return /* @__PURE__ */ new Set();
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
async function computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates = {
|
|
970
|
+
minStars: MIN_STARS,
|
|
971
|
+
minContributors: MIN_CONTRIBUTORS
|
|
972
|
+
}) {
|
|
945
973
|
const computedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
946
|
-
const empty = (status) => ({
|
|
947
|
-
status,
|
|
948
|
-
byDomain: {},
|
|
949
|
-
qualifyingTotal: 0,
|
|
950
|
-
computedAt
|
|
951
|
-
});
|
|
952
|
-
if (!token) return empty("no-token");
|
|
953
|
-
const ownedOrgs = await fetchOwnedOrgs(token);
|
|
954
974
|
const loginLc = login.toLowerCase();
|
|
955
975
|
let items;
|
|
956
976
|
try {
|
|
@@ -961,8 +981,9 @@ async function computeAcceptanceCredential(login, token, cache = /* @__PURE__ */
|
|
|
961
981
|
);
|
|
962
982
|
items = res.items ?? [];
|
|
963
983
|
} catch (err) {
|
|
964
|
-
const msg = String(err);
|
|
965
|
-
|
|
984
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
985
|
+
console.warn("[acceptance] search failed:", msg);
|
|
986
|
+
return emptyCredential(/HTTP 403|HTTP 429|rate limit/i.test(msg) ? "rate-limited" : "failed");
|
|
966
987
|
}
|
|
967
988
|
const byDomain = {};
|
|
968
989
|
let qualifyingTotal = 0;
|
|
@@ -976,8 +997,8 @@ async function computeAcceptanceCredential(login, token, cache = /* @__PURE__ */
|
|
|
976
997
|
const meta = await fetchRepoMeta(repo.owner, repo.name, token, cache);
|
|
977
998
|
if (!meta) continue;
|
|
978
999
|
if (meta.archived || meta.fork) continue;
|
|
979
|
-
if (meta.stars <
|
|
980
|
-
if (meta.contributors !== void 0 && meta.contributors <
|
|
1000
|
+
if (meta.stars < gates.minStars) continue;
|
|
1001
|
+
if (meta.contributors !== void 0 && meta.contributors < gates.minContributors) continue;
|
|
981
1002
|
qualifyingTotal += 1;
|
|
982
1003
|
const mergedAt = item.pull_request?.merged_at ?? item.closed_at ?? item.created_at;
|
|
983
1004
|
const rawDomains = [meta.language ?? "", ...meta.topics].filter(Boolean);
|
|
@@ -998,6 +1019,18 @@ async function computeAcceptanceCredential(login, token, cache = /* @__PURE__ */
|
|
|
998
1019
|
}
|
|
999
1020
|
return { status: "ok", byDomain: finalDomains, qualifyingTotal, computedAt };
|
|
1000
1021
|
}
|
|
1022
|
+
async function computeAcceptanceCredential(login, token, cache = /* @__PURE__ */ new Map()) {
|
|
1023
|
+
if (!token) return emptyCredential("no-token");
|
|
1024
|
+
const ownedOrgs = await fetchOwnedOrgs(token);
|
|
1025
|
+
return computeAcceptanceFromSearch(login, token, ownedOrgs, cache);
|
|
1026
|
+
}
|
|
1027
|
+
async function computeAcceptanceCredentialPublic(login, token, cache = /* @__PURE__ */ new Map(), opts) {
|
|
1028
|
+
if (!token) return emptyCredential("no-token");
|
|
1029
|
+
const ownedOrgs = await fetchPublicOrgs(login, token);
|
|
1030
|
+
for (const org of opts?.includeOrgs ?? []) ownedOrgs.delete(org.toLowerCase());
|
|
1031
|
+
const gates = opts?.relaxGates ? { minStars: 0, minContributors: 0 } : void 0;
|
|
1032
|
+
return computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates);
|
|
1033
|
+
}
|
|
1001
1034
|
function acceptanceCountForDomains(cred, domains) {
|
|
1002
1035
|
if (cred.status !== "ok") return 0;
|
|
1003
1036
|
let max = 0;
|
|
@@ -1016,7 +1049,60 @@ function bestAcceptanceDomain(cred, domains) {
|
|
|
1016
1049
|
}
|
|
1017
1050
|
return best;
|
|
1018
1051
|
}
|
|
1019
|
-
|
|
1052
|
+
function resumeRecencyDecay(lastSeenIso, now) {
|
|
1053
|
+
const ageMs = now - new Date(lastSeenIso).getTime();
|
|
1054
|
+
if (!Number.isFinite(ageMs)) return 0;
|
|
1055
|
+
return Math.pow(0.5, ageMs / RESUME_DECAY_HALF_LIFE_MS);
|
|
1056
|
+
}
|
|
1057
|
+
async function fetchRepoRecency(login, token) {
|
|
1058
|
+
try {
|
|
1059
|
+
const repos = await ghFetch(`/users/${login}/repos?sort=pushed&per_page=100`, token);
|
|
1060
|
+
return repos.filter((r) => !r.fork && !!r.pushed_at).map((r) => ({ pushedAt: r.pushed_at, language: r.language ?? null, topics: r.topics ?? [] }));
|
|
1061
|
+
} catch {
|
|
1062
|
+
return [];
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
function deriveResumeTrend(cred, repoRecency, now = Date.now()) {
|
|
1066
|
+
const agg = /* @__PURE__ */ new Map();
|
|
1067
|
+
const bump = (domain, when, count, mergedPRs) => {
|
|
1068
|
+
const e = agg.get(domain);
|
|
1069
|
+
if (!e) {
|
|
1070
|
+
agg.set(domain, { count, last: when, earliest: when, mergedPRs });
|
|
1071
|
+
} else {
|
|
1072
|
+
e.count += count;
|
|
1073
|
+
e.mergedPRs += mergedPRs;
|
|
1074
|
+
if (when > e.last) e.last = when;
|
|
1075
|
+
if (when < e.earliest) e.earliest = when;
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
if (cred.status === "ok") {
|
|
1079
|
+
for (const [domain, d] of Object.entries(cred.byDomain)) {
|
|
1080
|
+
bump(domain, d.lastMergedAt, d.mergedPRs, d.mergedPRs);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
for (const r of repoRecency) {
|
|
1084
|
+
for (const domain of new Set(normalize([r.language ?? "", ...r.topics].filter(Boolean)))) {
|
|
1085
|
+
bump(domain, r.pushedAt, 1, 0);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
const oneHalfLifeAgoIso = new Date(now - RESUME_DECAY_HALF_LIFE_MS).toISOString();
|
|
1089
|
+
const scored = [];
|
|
1090
|
+
for (const [domain, e] of agg.entries()) {
|
|
1091
|
+
const recencyScore2 = resumeRecencyDecay(e.last, now);
|
|
1092
|
+
const weight = e.count * recencyScore2;
|
|
1093
|
+
if (weight < RESUME_MIN_SCORE) continue;
|
|
1094
|
+
let direction;
|
|
1095
|
+
if (e.earliest > oneHalfLifeAgoIso) direction = "new";
|
|
1096
|
+
else if (recencyScore2 >= 0.5) direction = "up";
|
|
1097
|
+
else direction = "down";
|
|
1098
|
+
scored.push({
|
|
1099
|
+
t: { domain, direction, recencyScore: Math.round(recencyScore2 * 1e3) / 1e3, mergedPRs: e.mergedPRs },
|
|
1100
|
+
weight
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
return scored.sort((a, b) => b.weight - a.weight).slice(0, 12).map((s) => s.t);
|
|
1104
|
+
}
|
|
1105
|
+
var MIN_STARS, MIN_CONTRIBUTORS, CANDIDATE_PR_PAGE, TRIVIAL_PR_TITLE, RESUME_DECAY_HALF_LIFE_MS, RESUME_MIN_SCORE;
|
|
1020
1106
|
var init_github = __esm({
|
|
1021
1107
|
"../../packages/core/src/github.ts"() {
|
|
1022
1108
|
"use strict";
|
|
@@ -1025,6 +1111,8 @@ var init_github = __esm({
|
|
|
1025
1111
|
MIN_CONTRIBUTORS = 10;
|
|
1026
1112
|
CANDIDATE_PR_PAGE = 50;
|
|
1027
1113
|
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;
|
|
1114
|
+
RESUME_DECAY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
1115
|
+
RESUME_MIN_SCORE = 0.05;
|
|
1028
1116
|
}
|
|
1029
1117
|
});
|
|
1030
1118
|
|
|
@@ -1172,6 +1260,18 @@ var init_matcher = __esm({
|
|
|
1172
1260
|
}
|
|
1173
1261
|
});
|
|
1174
1262
|
|
|
1263
|
+
// ../../packages/core/src/feeds/http.ts
|
|
1264
|
+
function fetchWithTimeout(input, init, timeoutMs = FEED_FETCH_TIMEOUT_MS) {
|
|
1265
|
+
return fetch(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
1266
|
+
}
|
|
1267
|
+
var FEED_FETCH_TIMEOUT_MS;
|
|
1268
|
+
var init_http = __esm({
|
|
1269
|
+
"../../packages/core/src/feeds/http.ts"() {
|
|
1270
|
+
"use strict";
|
|
1271
|
+
FEED_FETCH_TIMEOUT_MS = 1e4;
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1175
1275
|
// ../../packages/core/src/feeds/greenhouse.ts
|
|
1176
1276
|
function extractTags(job) {
|
|
1177
1277
|
const body = [
|
|
@@ -1190,7 +1290,7 @@ async function fetchSlug(slug) {
|
|
|
1190
1290
|
const url = `https://boards-api.greenhouse.io/v1/boards/${slug}/jobs?content=true`;
|
|
1191
1291
|
let res;
|
|
1192
1292
|
try {
|
|
1193
|
-
res = await
|
|
1293
|
+
res = await fetchWithTimeout(url, { headers: { Accept: "application/json" } });
|
|
1194
1294
|
} catch (err) {
|
|
1195
1295
|
console.warn(`[greenhouse] ${slug}: network error \u2014`, err);
|
|
1196
1296
|
return [];
|
|
@@ -1232,6 +1332,7 @@ var init_greenhouse = __esm({
|
|
|
1232
1332
|
"../../packages/core/src/feeds/greenhouse.ts"() {
|
|
1233
1333
|
"use strict";
|
|
1234
1334
|
init_vocabulary();
|
|
1335
|
+
init_http();
|
|
1235
1336
|
FALLBACK_SLUGS = [
|
|
1236
1337
|
"stripe",
|
|
1237
1338
|
"linear",
|
|
@@ -1302,7 +1403,7 @@ function inferRemote2(job) {
|
|
|
1302
1403
|
}
|
|
1303
1404
|
async function fetchSlug2(slug) {
|
|
1304
1405
|
const url = `https://api.ashbyhq.com/posting-api/job-board/${slug}`;
|
|
1305
|
-
const res = await
|
|
1406
|
+
const res = await fetchWithTimeout(url, {
|
|
1306
1407
|
headers: { Accept: "application/json" }
|
|
1307
1408
|
});
|
|
1308
1409
|
if (!res.ok) {
|
|
@@ -1334,6 +1435,7 @@ var init_ashby = __esm({
|
|
|
1334
1435
|
"../../packages/core/src/feeds/ashby.ts"() {
|
|
1335
1436
|
"use strict";
|
|
1336
1437
|
init_vocabulary();
|
|
1438
|
+
init_http();
|
|
1337
1439
|
ashby = {
|
|
1338
1440
|
source: "ashby",
|
|
1339
1441
|
async fetch(opts) {
|
|
@@ -1384,7 +1486,7 @@ function toIso(ms) {
|
|
|
1384
1486
|
}
|
|
1385
1487
|
async function fetchSlug3(slug) {
|
|
1386
1488
|
const url = `https://api.lever.co/v0/postings/${slug}?mode=json`;
|
|
1387
|
-
const res = await
|
|
1489
|
+
const res = await fetchWithTimeout(url, { headers: { Accept: "application/json" } });
|
|
1388
1490
|
if (!res.ok) {
|
|
1389
1491
|
throw new Error(`Lever ${slug}: HTTP ${res.status}`);
|
|
1390
1492
|
}
|
|
@@ -1415,6 +1517,7 @@ var init_lever = __esm({
|
|
|
1415
1517
|
"../../packages/core/src/feeds/lever.ts"() {
|
|
1416
1518
|
"use strict";
|
|
1417
1519
|
init_vocabulary();
|
|
1520
|
+
init_http();
|
|
1418
1521
|
lever = {
|
|
1419
1522
|
source: "lever",
|
|
1420
1523
|
async fetch(opts) {
|
|
@@ -1456,12 +1559,13 @@ var init_himalayas = __esm({
|
|
|
1456
1559
|
"../../packages/core/src/feeds/himalayas.ts"() {
|
|
1457
1560
|
"use strict";
|
|
1458
1561
|
init_vocabulary();
|
|
1562
|
+
init_http();
|
|
1459
1563
|
himalayas = {
|
|
1460
1564
|
source: "himalayas",
|
|
1461
1565
|
async fetch(opts) {
|
|
1462
1566
|
const limit = opts?.limit ?? 100;
|
|
1463
1567
|
const url = `https://himalayas.app/jobs/api?limit=${limit}`;
|
|
1464
|
-
const res = await
|
|
1568
|
+
const res = await fetchWithTimeout(url, {
|
|
1465
1569
|
headers: { Accept: "application/json" }
|
|
1466
1570
|
});
|
|
1467
1571
|
if (!res.ok) {
|
|
@@ -1570,12 +1674,13 @@ var init_wwr = __esm({
|
|
|
1570
1674
|
"use strict";
|
|
1571
1675
|
init_vocabulary();
|
|
1572
1676
|
init_entities();
|
|
1677
|
+
init_http();
|
|
1573
1678
|
WWR_RSS_URL = "https://weworkremotely.com/remote-jobs.rss";
|
|
1574
1679
|
wwr = {
|
|
1575
1680
|
source: "wwr",
|
|
1576
1681
|
async fetch(opts) {
|
|
1577
1682
|
const limit = opts?.limit ?? 200;
|
|
1578
|
-
const res = await
|
|
1683
|
+
const res = await fetchWithTimeout(WWR_RSS_URL, {
|
|
1579
1684
|
headers: { Accept: "application/rss+xml, application/xml, text/xml" }
|
|
1580
1685
|
});
|
|
1581
1686
|
if (!res.ok) {
|
|
@@ -1583,6 +1688,11 @@ var init_wwr = __esm({
|
|
|
1583
1688
|
}
|
|
1584
1689
|
const xml = await res.text();
|
|
1585
1690
|
const items = parseRss(xml).slice(0, limit);
|
|
1691
|
+
function safeIso(s) {
|
|
1692
|
+
if (!s) return void 0;
|
|
1693
|
+
const d = new Date(s);
|
|
1694
|
+
return Number.isNaN(d.getTime()) ? void 0 : d.toISOString();
|
|
1695
|
+
}
|
|
1586
1696
|
return items.map((item) => ({
|
|
1587
1697
|
id: extractId(item.link),
|
|
1588
1698
|
source: "wwr",
|
|
@@ -1594,7 +1704,7 @@ var init_wwr = __esm({
|
|
|
1594
1704
|
location: "Remote",
|
|
1595
1705
|
tags: extractTags5(item),
|
|
1596
1706
|
roleType: inferRoleType(item.category),
|
|
1597
|
-
postedAt:
|
|
1707
|
+
postedAt: safeIso(item.pubDate),
|
|
1598
1708
|
applyMode: "direct",
|
|
1599
1709
|
raw: item
|
|
1600
1710
|
}));
|
|
@@ -1660,13 +1770,14 @@ var init_hn = __esm({
|
|
|
1660
1770
|
"use strict";
|
|
1661
1771
|
init_vocabulary();
|
|
1662
1772
|
init_entities();
|
|
1773
|
+
init_http();
|
|
1663
1774
|
ALGOLIA_SEARCH = "https://hn.algolia.com/api/v1/search?query=Ask+HN%3A+Who+is+Hiring%3F&tags=story,ask_hn&hitsPerPage=1";
|
|
1664
1775
|
ALGOLIA_ITEMS = "https://hn.algolia.com/api/v1/items/";
|
|
1665
1776
|
hn = {
|
|
1666
1777
|
source: "hn",
|
|
1667
1778
|
async fetch(opts) {
|
|
1668
1779
|
const limit = opts?.limit ?? 150;
|
|
1669
|
-
const searchRes = await
|
|
1780
|
+
const searchRes = await fetchWithTimeout(ALGOLIA_SEARCH, {
|
|
1670
1781
|
headers: { Accept: "application/json" }
|
|
1671
1782
|
});
|
|
1672
1783
|
if (!searchRes.ok) {
|
|
@@ -1677,7 +1788,7 @@ var init_hn = __esm({
|
|
|
1677
1788
|
if (!story) {
|
|
1678
1789
|
throw new Error('HN: No "Who is Hiring" story found');
|
|
1679
1790
|
}
|
|
1680
|
-
const itemRes = await
|
|
1791
|
+
const itemRes = await fetchWithTimeout(`${ALGOLIA_ITEMS}${story.objectID}`, {
|
|
1681
1792
|
headers: { Accept: "application/json" }
|
|
1682
1793
|
});
|
|
1683
1794
|
if (!itemRes.ok) {
|
|
@@ -1697,18 +1808,25 @@ var init_hn = __esm({
|
|
|
1697
1808
|
});
|
|
1698
1809
|
|
|
1699
1810
|
// ../../packages/core/src/feeds/bounty-gate.ts
|
|
1811
|
+
function isDenylistedRepo(fullName) {
|
|
1812
|
+
return DENYLIST_LC.has(fullName.toLowerCase());
|
|
1813
|
+
}
|
|
1814
|
+
function passesAntiFarm(amountUSD, stargazers) {
|
|
1815
|
+
return !(amountUSD > HIGH_VALUE_USD && stargazers < HIGH_VALUE_MIN_STARS);
|
|
1816
|
+
}
|
|
1700
1817
|
function ageDays(createdAtIso) {
|
|
1701
1818
|
const created = Date.parse(createdAtIso);
|
|
1702
1819
|
if (!Number.isFinite(created)) return 0;
|
|
1703
1820
|
return (Date.now() - created) / (1e3 * 60 * 60 * 24);
|
|
1704
1821
|
}
|
|
1705
1822
|
function passesMaturityGate(repo) {
|
|
1823
|
+
if (isDenylistedRepo(repo.fullName)) return false;
|
|
1706
1824
|
if (repo.archived || repo.disabled) return false;
|
|
1707
1825
|
if (repo.stargazers < MIN_REPO_STARS) return false;
|
|
1708
1826
|
if (ageDays(repo.createdAt) < MIN_REPO_AGE_DAYS) return false;
|
|
1709
1827
|
return true;
|
|
1710
1828
|
}
|
|
1711
|
-
var DEFAULT_BOUNTY_REPOS, MAX_BOUNTIES_PER_REPO, MIN_REPO_STARS, MIN_REPO_AGE_DAYS;
|
|
1829
|
+
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;
|
|
1712
1830
|
var init_bounty_gate = __esm({
|
|
1713
1831
|
"../../packages/core/src/feeds/bounty-gate.ts"() {
|
|
1714
1832
|
"use strict";
|
|
@@ -1725,8 +1843,13 @@ var init_bounty_gate = __esm({
|
|
|
1725
1843
|
"moorcheh-ai/memanto",
|
|
1726
1844
|
"PrismarineJS/mineflayer"
|
|
1727
1845
|
];
|
|
1846
|
+
BOUNTY_REPO_DENYLIST = ["SecureBananaLabs/bug-bounty"];
|
|
1847
|
+
DENYLIST_LC = new Set(BOUNTY_REPO_DENYLIST.map((r) => r.toLowerCase()));
|
|
1728
1848
|
MAX_BOUNTIES_PER_REPO = 10;
|
|
1849
|
+
MAX_BOUNTIES_PER_DISCOVERED_REPO = 3;
|
|
1729
1850
|
MIN_REPO_STARS = 5;
|
|
1851
|
+
HIGH_VALUE_USD = 500;
|
|
1852
|
+
HIGH_VALUE_MIN_STARS = 50;
|
|
1730
1853
|
MIN_REPO_AGE_DAYS = 30;
|
|
1731
1854
|
}
|
|
1732
1855
|
});
|
|
@@ -1771,7 +1894,7 @@ function isBountyIssue(issue) {
|
|
|
1771
1894
|
async function ghJson(path) {
|
|
1772
1895
|
let res;
|
|
1773
1896
|
try {
|
|
1774
|
-
res = await
|
|
1897
|
+
res = await fetchWithTimeout(`${GITHUB_API}${path}`, { headers: authHeaders() });
|
|
1775
1898
|
} catch (err) {
|
|
1776
1899
|
console.warn(`[github-bounties] network error ${path} \u2014`, err);
|
|
1777
1900
|
return null;
|
|
@@ -1853,31 +1976,435 @@ async function fetchRepoBounties(repoFullName) {
|
|
|
1853
1976
|
};
|
|
1854
1977
|
}));
|
|
1855
1978
|
}
|
|
1856
|
-
|
|
1979
|
+
function repoFullNameFromApiUrl(url) {
|
|
1980
|
+
const m = url.match(/\/repos\/([^/]+)\/([^/]+)\/?$/);
|
|
1981
|
+
return m ? `${m[1]}/${m[2]}` : null;
|
|
1982
|
+
}
|
|
1983
|
+
async function searchBountyIssues() {
|
|
1984
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
1985
|
+
for (const q of SEARCH_QUERIES) {
|
|
1986
|
+
const res = await ghJson(
|
|
1987
|
+
`/search/issues?q=${encodeURIComponent(q)}&sort=created&order=desc&per_page=${SEARCH_PER_PAGE}`
|
|
1988
|
+
);
|
|
1989
|
+
for (const it of res?.items ?? []) {
|
|
1990
|
+
if (it.pull_request) continue;
|
|
1991
|
+
if (!byUrl.has(it.html_url)) byUrl.set(it.html_url, it);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
return [...byUrl.values()].sort(
|
|
1995
|
+
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1998
|
+
async function repoMetaCached(fullName) {
|
|
1999
|
+
const hit = repoMetaCache.get(fullName);
|
|
2000
|
+
if (hit !== void 0) return hit;
|
|
2001
|
+
const r = await ghJson(`/repos/${fullName}`) ?? null;
|
|
2002
|
+
repoMetaCache.set(fullName, r);
|
|
2003
|
+
return r;
|
|
2004
|
+
}
|
|
2005
|
+
async function fetchRepoMeta2(fullName) {
|
|
2006
|
+
const repo = await repoMetaCached(fullName);
|
|
2007
|
+
if (!repo) return null;
|
|
2008
|
+
return {
|
|
2009
|
+
fullName: repo.full_name,
|
|
2010
|
+
stargazers: repo.stargazers_count,
|
|
2011
|
+
createdAt: repo.created_at,
|
|
2012
|
+
archived: repo.archived,
|
|
2013
|
+
disabled: repo.disabled
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
async function fetchRepoOpenPRRefs(fullName) {
|
|
2017
|
+
const hit = repoOpenPRRefsCache.get(fullName);
|
|
2018
|
+
if (hit !== void 0) return hit;
|
|
2019
|
+
const refs = /* @__PURE__ */ new Map();
|
|
2020
|
+
let scannedAny = false;
|
|
2021
|
+
for (let page = 1; page <= MAX_PR_PAGES; page++) {
|
|
2022
|
+
const prs = await ghJson(
|
|
2023
|
+
`/repos/${fullName}/pulls?state=open&per_page=100&page=${page}`
|
|
2024
|
+
);
|
|
2025
|
+
if (!Array.isArray(prs)) break;
|
|
2026
|
+
scannedAny = true;
|
|
2027
|
+
for (const pr of prs) {
|
|
2028
|
+
const counted = /* @__PURE__ */ new Set();
|
|
2029
|
+
for (const m of `${pr.title ?? ""}
|
|
2030
|
+
${pr.body ?? ""}`.matchAll(/#(\d+)\b/g)) {
|
|
2031
|
+
const n = Number(m[1]);
|
|
2032
|
+
if (!counted.has(n)) {
|
|
2033
|
+
counted.add(n);
|
|
2034
|
+
refs.set(n, (refs.get(n) ?? 0) + 1);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
if (prs.length < 100) break;
|
|
2039
|
+
}
|
|
2040
|
+
const result = scannedAny ? refs : null;
|
|
2041
|
+
repoOpenPRRefsCache.set(fullName, result);
|
|
2042
|
+
return result;
|
|
2043
|
+
}
|
|
2044
|
+
async function fetchIssueState(fullName, issueNumber) {
|
|
2045
|
+
const key = `${fullName}#${issueNumber}`;
|
|
2046
|
+
const hit = issueStateCache.get(key);
|
|
2047
|
+
if (hit !== void 0) return hit;
|
|
2048
|
+
const issue = await ghJson(`/repos/${fullName}/issues/${issueNumber}`);
|
|
2049
|
+
const state = issue?.state === "open" ? "open" : issue?.state === "closed" ? "closed" : null;
|
|
2050
|
+
issueStateCache.set(key, state);
|
|
2051
|
+
return state;
|
|
2052
|
+
}
|
|
2053
|
+
async function fetchSearchBounties() {
|
|
2054
|
+
const issues = (await searchBountyIssues()).slice(0, MAX_SEARCH_ISSUES_SCANNED);
|
|
2055
|
+
const distinctRepos = [
|
|
2056
|
+
...new Set(
|
|
2057
|
+
issues.map((i) => repoFullNameFromApiUrl(i.repository_url)).filter((x) => !!x)
|
|
2058
|
+
)
|
|
2059
|
+
];
|
|
2060
|
+
for (let i = 0; i < distinctRepos.length; i += REPO_META_CONCURRENCY) {
|
|
2061
|
+
await Promise.all(distinctRepos.slice(i, i + REPO_META_CONCURRENCY).map(repoMetaCached));
|
|
2062
|
+
}
|
|
2063
|
+
const jobs = [];
|
|
2064
|
+
const perRepo = /* @__PURE__ */ new Map();
|
|
2065
|
+
for (const issue of issues) {
|
|
2066
|
+
if (jobs.length >= MAX_SEARCH_BOUNTIES) break;
|
|
2067
|
+
const fullName = repoFullNameFromApiUrl(issue.repository_url);
|
|
2068
|
+
if (!fullName) continue;
|
|
2069
|
+
if ((perRepo.get(fullName) ?? 0) >= MAX_BOUNTIES_PER_REPO) continue;
|
|
2070
|
+
const repo = await repoMetaCached(fullName);
|
|
2071
|
+
if (!repo) continue;
|
|
2072
|
+
const passes = passesMaturityGate({
|
|
2073
|
+
fullName: repo.full_name,
|
|
2074
|
+
stargazers: repo.stargazers_count,
|
|
2075
|
+
createdAt: repo.created_at,
|
|
2076
|
+
archived: repo.archived,
|
|
2077
|
+
disabled: repo.disabled
|
|
2078
|
+
});
|
|
2079
|
+
if (!passes) continue;
|
|
2080
|
+
const title = decodeEntities(issue.title).trim();
|
|
2081
|
+
const body = issue.body ? decodeEntities(issue.body) : "";
|
|
2082
|
+
const labels = labelNames(issue);
|
|
2083
|
+
let amountUSD = parseAmountUSD(title) ?? parseAmountUSD(labels.join(" ")) ?? parseAmountUSD(body);
|
|
2084
|
+
if (amountUSD == null && labels.some((n) => /💎|💰/.test(n))) {
|
|
2085
|
+
amountUSD = await fetchCommentAmount(fullName, issue.number);
|
|
2086
|
+
}
|
|
2087
|
+
if (amountUSD == null) continue;
|
|
2088
|
+
if (!passesAntiFarm(amountUSD, repo.stargazers_count)) continue;
|
|
2089
|
+
const tags = normalize(
|
|
2090
|
+
tokenize2([title, labels.join(" "), body.slice(0, 2e3)].join(" "))
|
|
2091
|
+
);
|
|
2092
|
+
perRepo.set(fullName, (perRepo.get(fullName) ?? 0) + 1);
|
|
2093
|
+
jobs.push({
|
|
2094
|
+
id: `bounty:${fullName}#${issue.number}`,
|
|
2095
|
+
source: "bounty",
|
|
2096
|
+
title,
|
|
2097
|
+
company: repo.owner.login,
|
|
2098
|
+
url: issue.html_url,
|
|
2099
|
+
remote: true,
|
|
2100
|
+
location: "Remote",
|
|
2101
|
+
tags,
|
|
2102
|
+
roleType: "freelance",
|
|
2103
|
+
postedAt: issue.created_at,
|
|
2104
|
+
applyMode: "direct",
|
|
2105
|
+
bounty: {
|
|
2106
|
+
amountUSD,
|
|
2107
|
+
estimatedEffort: effortFromAmount(amountUSD),
|
|
2108
|
+
bountySource: "github",
|
|
2109
|
+
claimUrl: issue.html_url,
|
|
2110
|
+
repoFullName: fullName,
|
|
2111
|
+
repoStars: repo.stargazers_count,
|
|
2112
|
+
issueBody: body.slice(0, 1e3) || void 0
|
|
2113
|
+
},
|
|
2114
|
+
raw: issue
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
return jobs;
|
|
2118
|
+
}
|
|
2119
|
+
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;
|
|
1857
2120
|
var init_github_bounties = __esm({
|
|
1858
2121
|
"../../packages/core/src/feeds/github-bounties.ts"() {
|
|
1859
2122
|
"use strict";
|
|
1860
2123
|
init_vocabulary();
|
|
1861
2124
|
init_entities();
|
|
1862
2125
|
init_bounty_gate();
|
|
2126
|
+
init_http();
|
|
1863
2127
|
GITHUB_API = "https://api.github.com";
|
|
1864
2128
|
BOUNTY_LABEL_RE = /bounty|reward|funded|💎|💰/i;
|
|
2129
|
+
SEARCH_QUERIES = [
|
|
2130
|
+
'label:"\u{1F48E} Bounty" type:issue state:open',
|
|
2131
|
+
// Algora-applied — highest signal
|
|
2132
|
+
"label:bounty type:issue state:open",
|
|
2133
|
+
'label:"\u{1F4B0} Bounty" type:issue state:open'
|
|
2134
|
+
];
|
|
2135
|
+
SEARCH_PER_PAGE = 100;
|
|
2136
|
+
MAX_SEARCH_BOUNTIES = 150;
|
|
2137
|
+
MAX_SEARCH_ISSUES_SCANNED = 300;
|
|
2138
|
+
REPO_META_CONCURRENCY = 15;
|
|
2139
|
+
repoMetaCache = /* @__PURE__ */ new Map();
|
|
2140
|
+
MAX_PR_PAGES = 3;
|
|
2141
|
+
repoOpenPRRefsCache = /* @__PURE__ */ new Map();
|
|
2142
|
+
issueStateCache = /* @__PURE__ */ new Map();
|
|
1865
2143
|
githubBounties = {
|
|
1866
2144
|
source: "bounty",
|
|
1867
2145
|
async fetch(opts) {
|
|
1868
|
-
const
|
|
1869
|
-
|
|
1870
|
-
|
|
2146
|
+
const allowlist = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : DEFAULT_BOUNTY_REPOS;
|
|
2147
|
+
const [searched, listed] = await Promise.all([
|
|
2148
|
+
fetchSearchBounties().catch((e) => {
|
|
2149
|
+
console.warn("[github-bounties] search discovery failed:", e);
|
|
2150
|
+
return [];
|
|
2151
|
+
}),
|
|
2152
|
+
Promise.allSettled(allowlist.map(fetchRepoBounties)).then(
|
|
2153
|
+
(settled) => settled.flatMap((r) => r.status === "fulfilled" ? r.value : [])
|
|
2154
|
+
)
|
|
2155
|
+
]);
|
|
2156
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2157
|
+
const out = [];
|
|
2158
|
+
for (const j of [...searched, ...listed]) {
|
|
2159
|
+
if (!seen.has(j.id)) {
|
|
2160
|
+
seen.add(j.id);
|
|
2161
|
+
out.push(j);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
console.info(
|
|
2165
|
+
`[github-bounties] total: ${out.length} bounties (${searched.length} search + ${listed.length} allowlist, deduped)`
|
|
2166
|
+
);
|
|
2167
|
+
return out;
|
|
2168
|
+
}
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
});
|
|
2172
|
+
|
|
2173
|
+
// ../../packages/core/src/feeds/opire.ts
|
|
2174
|
+
function tokenize3(text) {
|
|
2175
|
+
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter((w) => w.length > 1);
|
|
2176
|
+
}
|
|
2177
|
+
function effortFromAmount2(usd) {
|
|
2178
|
+
if (usd == null) return void 0;
|
|
2179
|
+
if (usd < 150) return "small";
|
|
2180
|
+
if (usd < 750) return "medium";
|
|
2181
|
+
return "large";
|
|
2182
|
+
}
|
|
2183
|
+
function priceToUSD(p) {
|
|
2184
|
+
if (!p || typeof p.value !== "number") return void 0;
|
|
2185
|
+
if (p.unit === "USD_CENT") return Math.round(p.value) / 100;
|
|
2186
|
+
if (p.unit === "USD") return p.value;
|
|
2187
|
+
return void 0;
|
|
2188
|
+
}
|
|
2189
|
+
function repoFullNameFromUrl(url) {
|
|
2190
|
+
const m = url?.match(/github\.com\/([^/]+)\/([^/]+)/i);
|
|
2191
|
+
return m ? `${m[1]}/${m[2].replace(/\.git$/, "")}` : void 0;
|
|
2192
|
+
}
|
|
2193
|
+
function issueNumberFromUrl(url) {
|
|
2194
|
+
const m = url?.match(/\/issues\/(\d+)/);
|
|
2195
|
+
return m ? parseInt(m[1], 10) : void 0;
|
|
2196
|
+
}
|
|
2197
|
+
var OPIRE_REWARDS_URL, MIN_USD, MAX_USD, MAX_OPIRE_BOUNTIES, REPO_META_CONCURRENCY2, opire;
|
|
2198
|
+
var init_opire = __esm({
|
|
2199
|
+
"../../packages/core/src/feeds/opire.ts"() {
|
|
2200
|
+
"use strict";
|
|
2201
|
+
init_vocabulary();
|
|
2202
|
+
init_bounty_gate();
|
|
2203
|
+
init_github_bounties();
|
|
2204
|
+
init_http();
|
|
2205
|
+
OPIRE_REWARDS_URL = "https://api.opire.dev/rewards";
|
|
2206
|
+
MIN_USD = 25;
|
|
2207
|
+
MAX_USD = 25e3;
|
|
2208
|
+
MAX_OPIRE_BOUNTIES = 100;
|
|
2209
|
+
REPO_META_CONCURRENCY2 = 15;
|
|
2210
|
+
opire = {
|
|
2211
|
+
source: "bounty",
|
|
2212
|
+
async fetch() {
|
|
2213
|
+
let rewards;
|
|
2214
|
+
try {
|
|
2215
|
+
const res = await fetchWithTimeout(OPIRE_REWARDS_URL, {
|
|
2216
|
+
headers: { Accept: "application/json", "User-Agent": "terminalhire" }
|
|
2217
|
+
});
|
|
2218
|
+
if (!res.ok) {
|
|
2219
|
+
console.warn(`[opire] HTTP ${res.status}`);
|
|
2220
|
+
return [];
|
|
2221
|
+
}
|
|
2222
|
+
const json = await res.json();
|
|
2223
|
+
rewards = Array.isArray(json) ? json : json?.data ?? json?.items ?? [];
|
|
2224
|
+
} catch (err) {
|
|
2225
|
+
console.warn("[opire] fetch failed \u2014", err);
|
|
2226
|
+
return [];
|
|
2227
|
+
}
|
|
2228
|
+
const candidates = [];
|
|
2229
|
+
for (const r of rewards) {
|
|
2230
|
+
if (r.platform !== "GitHub") continue;
|
|
2231
|
+
if (r.project && r.project.isPublic === false) continue;
|
|
2232
|
+
const repoFullName = repoFullNameFromUrl(r.project?.url ?? r.url);
|
|
2233
|
+
if (!repoFullName) continue;
|
|
2234
|
+
const amountUSD = priceToUSD(r.pendingPrice);
|
|
2235
|
+
if (amountUSD == null || amountUSD < MIN_USD || amountUSD > MAX_USD) continue;
|
|
2236
|
+
const title = (r.title ?? "").trim();
|
|
2237
|
+
if (title.length < 4) continue;
|
|
2238
|
+
const tags = normalize([...r.programmingLanguages ?? [], ...tokenize3(title)]);
|
|
2239
|
+
const bounty = {
|
|
2240
|
+
amountUSD,
|
|
2241
|
+
estimatedEffort: effortFromAmount2(amountUSD),
|
|
2242
|
+
bountySource: "opire",
|
|
2243
|
+
claimUrl: r.url,
|
|
2244
|
+
repoFullName
|
|
2245
|
+
};
|
|
2246
|
+
candidates.push({
|
|
2247
|
+
repoFullName,
|
|
2248
|
+
amountUSD,
|
|
2249
|
+
issueNumber: issueNumberFromUrl(r.url),
|
|
2250
|
+
bounty,
|
|
2251
|
+
job: {
|
|
2252
|
+
id: `bounty:opire:${r.id}`,
|
|
2253
|
+
source: "bounty",
|
|
2254
|
+
title,
|
|
2255
|
+
company: r.organization?.name ?? repoFullName.split("/")[0],
|
|
2256
|
+
url: r.url,
|
|
2257
|
+
remote: true,
|
|
2258
|
+
location: "Remote",
|
|
2259
|
+
tags,
|
|
2260
|
+
roleType: "freelance",
|
|
2261
|
+
postedAt: Number.isFinite(r.createdAt) ? new Date(r.createdAt).toISOString() : void 0,
|
|
2262
|
+
applyMode: "direct",
|
|
2263
|
+
bounty,
|
|
2264
|
+
raw: r
|
|
2265
|
+
}
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
const distinctRepos = [...new Set(candidates.map((c) => c.repoFullName))];
|
|
2269
|
+
const meta = /* @__PURE__ */ new Map();
|
|
2270
|
+
for (let i = 0; i < distinctRepos.length; i += REPO_META_CONCURRENCY2) {
|
|
2271
|
+
const batch = distinctRepos.slice(i, i + REPO_META_CONCURRENCY2);
|
|
2272
|
+
const metas = await Promise.all(batch.map((name) => fetchRepoMeta2(name)));
|
|
2273
|
+
batch.forEach((name, k) => meta.set(name, metas[k]));
|
|
2274
|
+
}
|
|
2275
|
+
const gated = [];
|
|
2276
|
+
let dropped = 0;
|
|
2277
|
+
let ungated = 0;
|
|
2278
|
+
for (const c of candidates) {
|
|
2279
|
+
const m = meta.get(c.repoFullName);
|
|
2280
|
+
if (m) {
|
|
2281
|
+
if (!passesMaturityGate(m) || !passesAntiFarm(c.amountUSD, m.stargazers)) {
|
|
2282
|
+
dropped++;
|
|
2283
|
+
continue;
|
|
2284
|
+
}
|
|
2285
|
+
c.bounty.repoStars = m.stargazers;
|
|
2286
|
+
} else {
|
|
2287
|
+
ungated++;
|
|
2288
|
+
}
|
|
2289
|
+
gated.push(c);
|
|
2290
|
+
}
|
|
2291
|
+
const issueState = /* @__PURE__ */ new Map();
|
|
2292
|
+
for (let i = 0; i < gated.length; i += REPO_META_CONCURRENCY2) {
|
|
2293
|
+
const batch = gated.slice(i, i + REPO_META_CONCURRENCY2);
|
|
2294
|
+
const states = await Promise.all(
|
|
2295
|
+
batch.map(
|
|
2296
|
+
(c) => c.issueNumber != null ? fetchIssueState(c.repoFullName, c.issueNumber) : Promise.resolve(null)
|
|
2297
|
+
)
|
|
2298
|
+
);
|
|
2299
|
+
batch.forEach((c, k) => issueState.set(c.job.id, states[k]));
|
|
2300
|
+
}
|
|
2301
|
+
const jobs = [];
|
|
2302
|
+
let closed = 0;
|
|
2303
|
+
for (const c of gated) {
|
|
2304
|
+
if (jobs.length >= MAX_OPIRE_BOUNTIES) break;
|
|
2305
|
+
if (issueState.get(c.job.id) === "closed") {
|
|
2306
|
+
closed++;
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
2309
|
+
jobs.push(c.job);
|
|
2310
|
+
}
|
|
2311
|
+
console.info(
|
|
2312
|
+
`[opire] ${jobs.length} bounties (from ${rewards.length} rewards; ${dropped} repo-gated, ${closed} closed-issue, ${ungated} kept ungated)`
|
|
2313
|
+
);
|
|
2314
|
+
return jobs;
|
|
2315
|
+
}
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
});
|
|
2319
|
+
|
|
2320
|
+
// ../../packages/core/src/feeds/workable.ts
|
|
2321
|
+
function locationStr(loc) {
|
|
2322
|
+
if (!loc) return "";
|
|
2323
|
+
return [loc.city, loc.country].filter(Boolean).join(", ");
|
|
2324
|
+
}
|
|
2325
|
+
function isRemote(j) {
|
|
2326
|
+
return j.remote === true || (j.workplace ?? "").toLowerCase() === "remote";
|
|
2327
|
+
}
|
|
2328
|
+
function extractTags7(j) {
|
|
2329
|
+
const body = [...j.department ?? [], locationStr(j.location)].filter(Boolean).join(" ");
|
|
2330
|
+
return extractSkillTags(j.title, body);
|
|
2331
|
+
}
|
|
2332
|
+
async function fetchAccount(account) {
|
|
2333
|
+
const url = `https://apply.workable.com/api/v3/accounts/${account}/jobs`;
|
|
2334
|
+
const out = [];
|
|
2335
|
+
let token;
|
|
2336
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
2337
|
+
let res;
|
|
2338
|
+
try {
|
|
2339
|
+
res = await fetchWithTimeout(url, {
|
|
2340
|
+
method: "POST",
|
|
2341
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
2342
|
+
body: JSON.stringify(token ? { token } : {})
|
|
2343
|
+
});
|
|
2344
|
+
} catch (err) {
|
|
2345
|
+
console.warn(`[workable] ${account}: network error \u2014`, err);
|
|
2346
|
+
break;
|
|
2347
|
+
}
|
|
2348
|
+
if (!res.ok) {
|
|
2349
|
+
console.warn(`[workable] ${account}: HTTP ${res.status}`);
|
|
2350
|
+
break;
|
|
2351
|
+
}
|
|
2352
|
+
let data;
|
|
2353
|
+
try {
|
|
2354
|
+
data = await res.json();
|
|
2355
|
+
} catch (err) {
|
|
2356
|
+
console.warn(`[workable] ${account}: JSON parse error \u2014`, err);
|
|
2357
|
+
break;
|
|
2358
|
+
}
|
|
2359
|
+
const results = data.results ?? [];
|
|
2360
|
+
for (const j of results) {
|
|
2361
|
+
if (j.state && j.state !== "published") continue;
|
|
2362
|
+
out.push({
|
|
2363
|
+
id: `workable:${j.id}`,
|
|
2364
|
+
source: "workable",
|
|
2365
|
+
title: j.title,
|
|
2366
|
+
company: account,
|
|
2367
|
+
url: `https://apply.workable.com/${account}/j/${j.shortcode}/`,
|
|
2368
|
+
remote: isRemote(j),
|
|
2369
|
+
location: locationStr(j.location) || void 0,
|
|
2370
|
+
tags: extractTags7(j),
|
|
2371
|
+
roleType: "full_time",
|
|
2372
|
+
postedAt: j.published,
|
|
2373
|
+
applyMode: "direct",
|
|
2374
|
+
raw: j
|
|
2375
|
+
});
|
|
2376
|
+
}
|
|
2377
|
+
token = data.token;
|
|
2378
|
+
if (!token || results.length === 0) break;
|
|
2379
|
+
}
|
|
2380
|
+
if (out.length > 0) console.info(`[workable] ${account}: ${out.length} jobs`);
|
|
2381
|
+
return out;
|
|
2382
|
+
}
|
|
2383
|
+
var FALLBACK_ACCOUNTS, MAX_PAGES, workable;
|
|
2384
|
+
var init_workable = __esm({
|
|
2385
|
+
"../../packages/core/src/feeds/workable.ts"() {
|
|
2386
|
+
"use strict";
|
|
2387
|
+
init_vocabulary();
|
|
2388
|
+
init_http();
|
|
2389
|
+
FALLBACK_ACCOUNTS = ["zego", "workmotion"];
|
|
2390
|
+
MAX_PAGES = 5;
|
|
2391
|
+
workable = {
|
|
2392
|
+
source: "workable",
|
|
2393
|
+
async fetch(opts) {
|
|
2394
|
+
const accounts = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : FALLBACK_ACCOUNTS;
|
|
2395
|
+
console.info(`[workable] fetching ${accounts.length} accounts: ${accounts.join(", ")}`);
|
|
2396
|
+
const results = await Promise.allSettled(accounts.map(fetchAccount));
|
|
1871
2397
|
const jobs = [];
|
|
1872
2398
|
let failures = 0;
|
|
1873
|
-
for (const r of
|
|
1874
|
-
if (r.status === "fulfilled")
|
|
1875
|
-
|
|
2399
|
+
for (const r of results) {
|
|
2400
|
+
if (r.status === "fulfilled") {
|
|
2401
|
+
jobs.push(...r.value);
|
|
2402
|
+
} else {
|
|
1876
2403
|
failures++;
|
|
1877
|
-
console.warn("[
|
|
2404
|
+
console.warn("[workable] account fetch rejected:", r.reason);
|
|
1878
2405
|
}
|
|
1879
2406
|
}
|
|
1880
|
-
console.info(`[
|
|
2407
|
+
console.info(`[workable] total: ${jobs.length} jobs, ${failures} account failures`);
|
|
1881
2408
|
return jobs;
|
|
1882
2409
|
}
|
|
1883
2410
|
};
|
|
@@ -1886,7 +2413,57 @@ var init_github_bounties = __esm({
|
|
|
1886
2413
|
|
|
1887
2414
|
// ../../packages/core/src/feeds/index.ts
|
|
1888
2415
|
async function aggregateBounties(opts) {
|
|
1889
|
-
|
|
2416
|
+
const [gh, op] = await Promise.all([
|
|
2417
|
+
githubBounties.fetch({ slugs: opts?.repos }),
|
|
2418
|
+
opire.fetch()
|
|
2419
|
+
]);
|
|
2420
|
+
const allowlist = new Set(
|
|
2421
|
+
(opts?.repos && opts.repos.length > 0 ? opts.repos : DEFAULT_BOUNTY_REPOS).map(
|
|
2422
|
+
(r) => r.toLowerCase()
|
|
2423
|
+
)
|
|
2424
|
+
);
|
|
2425
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2426
|
+
const perRepo = /* @__PURE__ */ new Map();
|
|
2427
|
+
const seenRepoTitles = /* @__PURE__ */ new Set();
|
|
2428
|
+
const out = [];
|
|
2429
|
+
for (const j of [...gh, ...op]) {
|
|
2430
|
+
const key = j.bounty?.claimUrl ?? j.url;
|
|
2431
|
+
if (seen.has(key)) continue;
|
|
2432
|
+
const repo = j.bounty?.repoFullName?.toLowerCase();
|
|
2433
|
+
if (repo) {
|
|
2434
|
+
if (isDenylistedRepo(repo)) continue;
|
|
2435
|
+
const titleKey = `${repo} ${normalizeBountyTitle(j.title)}`;
|
|
2436
|
+
if (seenRepoTitles.has(titleKey)) continue;
|
|
2437
|
+
const cap = allowlist.has(repo) ? MAX_BOUNTIES_PER_REPO : MAX_BOUNTIES_PER_DISCOVERED_REPO;
|
|
2438
|
+
const n = perRepo.get(repo) ?? 0;
|
|
2439
|
+
if (n >= cap) continue;
|
|
2440
|
+
perRepo.set(repo, n + 1);
|
|
2441
|
+
seenRepoTitles.add(titleKey);
|
|
2442
|
+
}
|
|
2443
|
+
seen.add(key);
|
|
2444
|
+
out.push(j);
|
|
2445
|
+
}
|
|
2446
|
+
const repos = [...new Set(out.map((j) => j.bounty?.repoFullName).filter((r) => !!r))];
|
|
2447
|
+
const refsByRepo = /* @__PURE__ */ new Map();
|
|
2448
|
+
const PR_REFS_CONCURRENCY = 15;
|
|
2449
|
+
for (let i = 0; i < repos.length; i += PR_REFS_CONCURRENCY) {
|
|
2450
|
+
const batch = repos.slice(i, i + PR_REFS_CONCURRENCY);
|
|
2451
|
+
const results = await Promise.all(batch.map((r) => fetchRepoOpenPRRefs(r)));
|
|
2452
|
+
batch.forEach((r, k) => refsByRepo.set(r, results[k]));
|
|
2453
|
+
}
|
|
2454
|
+
for (const j of out) {
|
|
2455
|
+
const num = bountyIssueNumber(j.bounty?.claimUrl);
|
|
2456
|
+
const refs = j.bounty?.repoFullName ? refsByRepo.get(j.bounty.repoFullName) : void 0;
|
|
2457
|
+
if (j.bounty && refs && num != null) j.bounty.competingOpenPRs = refs.get(num) ?? 0;
|
|
2458
|
+
}
|
|
2459
|
+
return out;
|
|
2460
|
+
}
|
|
2461
|
+
function bountyIssueNumber(url) {
|
|
2462
|
+
const m = url?.match(/\/issues\/(\d+)/);
|
|
2463
|
+
return m ? Number(m[1]) : void 0;
|
|
2464
|
+
}
|
|
2465
|
+
function normalizeBountyTitle(title) {
|
|
2466
|
+
return title.toLowerCase().replace(/#\d+\s*$/, "").replace(/[^a-z0-9]+/g, " ").trim();
|
|
1890
2467
|
}
|
|
1891
2468
|
function flattenTiers(t) {
|
|
1892
2469
|
return [.../* @__PURE__ */ new Set([...t.bigco, ...t.scaleup, ...t.startup])];
|
|
@@ -1895,18 +2472,20 @@ async function aggregate(opts) {
|
|
|
1895
2472
|
const ghSlugs = opts?.slugs?.["greenhouse"] ?? DEFAULT_GREENHOUSE_SLUGS;
|
|
1896
2473
|
const ashbySlugs = opts?.slugs?.["ashby"] ?? DEFAULT_ASHBY_SLUGS;
|
|
1897
2474
|
const leverSlugs = opts?.slugs?.["lever"] ?? DEFAULT_LEVER_SLUGS;
|
|
2475
|
+
const workableSlugs = opts?.slugs?.["workable"] ?? DEFAULT_WORKABLE_SLUGS;
|
|
1898
2476
|
const limit = opts?.limit ?? 150;
|
|
1899
2477
|
const settled = await Promise.allSettled([
|
|
1900
2478
|
greenhouse.fetch({ slugs: ghSlugs, limit }),
|
|
1901
2479
|
ashby.fetch({ slugs: ashbySlugs, limit }),
|
|
1902
2480
|
lever.fetch({ slugs: leverSlugs, limit }),
|
|
2481
|
+
workable.fetch({ slugs: workableSlugs, limit }),
|
|
1903
2482
|
himalayas.fetch({ limit }),
|
|
1904
2483
|
wwr.fetch({ limit }),
|
|
1905
2484
|
hn.fetch({ limit })
|
|
1906
2485
|
]);
|
|
1907
2486
|
const seen = /* @__PURE__ */ new Set();
|
|
1908
2487
|
const jobs = [];
|
|
1909
|
-
const sourceNames = ["greenhouse", "ashby", "lever", "himalayas", "wwr", "hn"];
|
|
2488
|
+
const sourceNames = ["greenhouse", "ashby", "lever", "workable", "himalayas", "wwr", "hn"];
|
|
1910
2489
|
for (let i = 0; i < settled.length; i++) {
|
|
1911
2490
|
const result = settled[i];
|
|
1912
2491
|
if (result.status === "rejected") {
|
|
@@ -1922,7 +2501,7 @@ async function aggregate(opts) {
|
|
|
1922
2501
|
}
|
|
1923
2502
|
if (opts?.includeBounties !== false) {
|
|
1924
2503
|
try {
|
|
1925
|
-
const bounties = await
|
|
2504
|
+
const bounties = await aggregateBounties({ repos: opts?.slugs?.["bounty"] });
|
|
1926
2505
|
for (const b of bounties) {
|
|
1927
2506
|
if (!seen.has(b.id)) {
|
|
1928
2507
|
seen.add(b.id);
|
|
@@ -1935,7 +2514,7 @@ async function aggregate(opts) {
|
|
|
1935
2514
|
}
|
|
1936
2515
|
return jobs;
|
|
1937
2516
|
}
|
|
1938
|
-
var FEEDS, GREENHOUSE_SLUGS_BY_TIER, ASHBY_SLUGS_BY_TIER, LEVER_SLUGS_BY_TIER, DEFAULT_GREENHOUSE_SLUGS, DEFAULT_ASHBY_SLUGS, DEFAULT_LEVER_SLUGS;
|
|
2517
|
+
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;
|
|
1939
2518
|
var init_feeds = __esm({
|
|
1940
2519
|
"../../packages/core/src/feeds/index.ts"() {
|
|
1941
2520
|
"use strict";
|
|
@@ -1946,8 +2525,11 @@ var init_feeds = __esm({
|
|
|
1946
2525
|
init_wwr();
|
|
1947
2526
|
init_hn();
|
|
1948
2527
|
init_github_bounties();
|
|
2528
|
+
init_opire();
|
|
2529
|
+
init_workable();
|
|
1949
2530
|
init_bounty_gate();
|
|
1950
|
-
|
|
2531
|
+
init_bounty_gate();
|
|
2532
|
+
FEEDS = [greenhouse, ashby, lever, workable, himalayas, wwr, hn];
|
|
1951
2533
|
GREENHOUSE_SLUGS_BY_TIER = {
|
|
1952
2534
|
bigco: [
|
|
1953
2535
|
"stripe",
|
|
@@ -2053,6 +2635,7 @@ var init_feeds = __esm({
|
|
|
2053
2635
|
DEFAULT_GREENHOUSE_SLUGS = flattenTiers(GREENHOUSE_SLUGS_BY_TIER);
|
|
2054
2636
|
DEFAULT_ASHBY_SLUGS = flattenTiers(ASHBY_SLUGS_BY_TIER);
|
|
2055
2637
|
DEFAULT_LEVER_SLUGS = flattenTiers(LEVER_SLUGS_BY_TIER);
|
|
2638
|
+
DEFAULT_WORKABLE_SLUGS = ["zego", "workmotion"];
|
|
2056
2639
|
}
|
|
2057
2640
|
});
|
|
2058
2641
|
|
|
@@ -2153,6 +2736,7 @@ __export(src_exports, {
|
|
|
2153
2736
|
DEFAULT_BOUNTY_REPOS: () => DEFAULT_BOUNTY_REPOS,
|
|
2154
2737
|
DEFAULT_GREENHOUSE_SLUGS: () => DEFAULT_GREENHOUSE_SLUGS,
|
|
2155
2738
|
DEFAULT_LEVER_SLUGS: () => DEFAULT_LEVER_SLUGS,
|
|
2739
|
+
DEFAULT_WORKABLE_SLUGS: () => DEFAULT_WORKABLE_SLUGS,
|
|
2156
2740
|
EXAMPLE_BUYER: () => EXAMPLE_BUYER,
|
|
2157
2741
|
FEEDS: () => FEEDS,
|
|
2158
2742
|
GRAPH: () => GRAPH,
|
|
@@ -2171,10 +2755,13 @@ __export(src_exports, {
|
|
|
2171
2755
|
buildIndex: () => buildIndex,
|
|
2172
2756
|
buildReason: () => buildReason,
|
|
2173
2757
|
computeAcceptanceCredential: () => computeAcceptanceCredential,
|
|
2758
|
+
computeAcceptanceCredentialPublic: () => computeAcceptanceCredentialPublic,
|
|
2174
2759
|
coreTagsFromTitle: () => coreTagsFromTitle,
|
|
2760
|
+
deriveResumeTrend: () => deriveResumeTrend,
|
|
2175
2761
|
expandWeighted: () => expandWeighted,
|
|
2176
2762
|
extractSkillTags: () => extractSkillTags,
|
|
2177
2763
|
fetchGitHubProfile: () => fetchGitHubProfile,
|
|
2764
|
+
fetchRepoRecency: () => fetchRepoRecency,
|
|
2178
2765
|
flattenTiers: () => flattenTiers,
|
|
2179
2766
|
getBuyer: () => getBuyer,
|
|
2180
2767
|
githubBounties: () => githubBounties,
|
|
@@ -2188,9 +2775,11 @@ __export(src_exports, {
|
|
|
2188
2775
|
looksLikeEngRole: () => looksLikeEngRole,
|
|
2189
2776
|
match: () => match,
|
|
2190
2777
|
normalize: () => normalize,
|
|
2778
|
+
opire: () => opire,
|
|
2191
2779
|
passesMaturityGate: () => passesMaturityGate,
|
|
2192
2780
|
tokenize: () => tokenize,
|
|
2193
2781
|
validateGraph: () => validateGraph,
|
|
2782
|
+
workable: () => workable,
|
|
2194
2783
|
wwr: () => wwr
|
|
2195
2784
|
});
|
|
2196
2785
|
var init_src = __esm({
|
|
@@ -2480,11 +3069,11 @@ async function runLogin() {
|
|
|
2480
3069
|
if (process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["JPI_GITHUB_MOCK"] === "1") {
|
|
2481
3070
|
const { createRequire: createRequire2 } = await import("module");
|
|
2482
3071
|
const { fileURLToPath: fileURLToPath7 } = await import("url");
|
|
2483
|
-
const { join:
|
|
3072
|
+
const { join: join17, dirname: dirname3 } = await import("path");
|
|
2484
3073
|
const __dirname6 = fileURLToPath7(new URL(".", import.meta.url));
|
|
2485
|
-
const fixturePath =
|
|
2486
|
-
const { readFileSync:
|
|
2487
|
-
ghProfile = JSON.parse(
|
|
3074
|
+
const fixturePath = join17(__dirname6, "../../fixtures/github-sample.json");
|
|
3075
|
+
const { readFileSync: readFileSync16 } = await import("fs");
|
|
3076
|
+
ghProfile = JSON.parse(readFileSync16(fixturePath, "utf8"));
|
|
2488
3077
|
} else {
|
|
2489
3078
|
ghProfile = await fetchGitHubProfile2(login, token);
|
|
2490
3079
|
}
|
|
@@ -2882,9 +3471,11 @@ function printBounty(i, job, score, reason, matchedTags) {
|
|
|
2882
3471
|
const stars = b.repoStars != null ? ` \xB7 ${b.repoStars}\u2605` : "";
|
|
2883
3472
|
const effort = b.estimatedEffort ? ` \xB7 ${EFFORT_LABEL[b.estimatedEffort]}` : "";
|
|
2884
3473
|
const scoreStr = score > 0 ? ` \xB7 match ${Math.round(score * 100)}%` : "";
|
|
3474
|
+
const prs = b.competingOpenPRs;
|
|
3475
|
+
const contend = prs != null && prs > 0 ? ` \xB7 \u26A0 ${prs} PR${prs === 1 ? "" : "s"} in flight` : "";
|
|
2885
3476
|
console.log(`
|
|
2886
3477
|
${i + 1}. ${linkTitle2(job.title, job.url)}`);
|
|
2887
|
-
console.log(` ${formatAmount(b)}${effort} \xB7 ${b.repoFullName ?? job.company}${stars}${scoreStr}`);
|
|
3478
|
+
console.log(` ${formatAmount(b)}${effort} \xB7 ${b.repoFullName ?? job.company}${stars}${scoreStr}${contend}`);
|
|
2888
3479
|
if (reason) console.log(` ${reason}`);
|
|
2889
3480
|
if (matchedTags && matchedTags.length) console.log(` Tags matched: ${matchedTags.slice(0, 5).join(", ")}`);
|
|
2890
3481
|
console.log(` id: ${job.id}`);
|
|
@@ -2896,6 +3487,7 @@ async function run3() {
|
|
|
2896
3487
|
const index = await fetchIndex2();
|
|
2897
3488
|
let bounties = (index.jobs ?? []).filter((j) => j.source === "bounty");
|
|
2898
3489
|
if (PRICED_ONLY) bounties = bounties.filter((j) => j.bounty?.amountUSD != null);
|
|
3490
|
+
if (WINNABLE_ONLY) bounties = bounties.filter((j) => (j.bounty?.competingOpenPRs ?? 0) === 0);
|
|
2899
3491
|
if (bounties.length === 0) {
|
|
2900
3492
|
console.log("\nNo bounties available right now. Try again later \u2014 supply refreshes through the day.");
|
|
2901
3493
|
return;
|
|
@@ -2915,7 +3507,8 @@ async function run3() {
|
|
|
2915
3507
|
}
|
|
2916
3508
|
const score = (j) => ranked.get(j.id)?.score ?? 0;
|
|
2917
3509
|
const amt = (j) => j.bounty?.amountUSD ?? -1;
|
|
2918
|
-
|
|
3510
|
+
const contested = (j) => (j.bounty?.competingOpenPRs ?? 0) > 0 ? 1 : 0;
|
|
3511
|
+
bounties.sort((a, b) => contested(a) - contested(b) || score(b) - score(a) || amt(b) - amt(a));
|
|
2919
3512
|
const shown = SHOW_ALL2 ? bounties : bounties.slice(0, LIMIT2);
|
|
2920
3513
|
const matchedCount = bounties.filter((j) => score(j) > 0).length;
|
|
2921
3514
|
console.log(
|
|
@@ -2948,7 +3541,7 @@ Open this to claim/work the bounty (you go straight to the source \u2014 we neve
|
|
|
2948
3541
|
process.exit(1);
|
|
2949
3542
|
}
|
|
2950
3543
|
}
|
|
2951
|
-
var TERMINALHIRE_DIR4, INDEX_CACHE_FILE2, INDEX_TTL_MS2, API_URL2, DEFAULT_LIMIT2, args2, limitArg2, LIMIT2, PRICED_ONLY, SHOW_ALL2, EFFORT_LABEL;
|
|
3544
|
+
var TERMINALHIRE_DIR4, INDEX_CACHE_FILE2, INDEX_TTL_MS2, API_URL2, DEFAULT_LIMIT2, args2, limitArg2, LIMIT2, PRICED_ONLY, SHOW_ALL2, WINNABLE_ONLY, EFFORT_LABEL;
|
|
2952
3545
|
var init_jpi_bounties = __esm({
|
|
2953
3546
|
"bin/jpi-bounties.js"() {
|
|
2954
3547
|
"use strict";
|
|
@@ -2962,14 +3555,378 @@ var init_jpi_bounties = __esm({
|
|
|
2962
3555
|
LIMIT2 = limitArg2 !== -1 ? parseInt(args2[limitArg2 + 1] ?? "15", 10) : DEFAULT_LIMIT2;
|
|
2963
3556
|
PRICED_ONLY = args2.includes("--priced");
|
|
2964
3557
|
SHOW_ALL2 = args2.includes("--all");
|
|
3558
|
+
WINNABLE_ONLY = args2.includes("--winnable");
|
|
2965
3559
|
EFFORT_LABEL = { small: "small (~\xBD day)", medium: "medium (~1 day)", large: "large (multi-day)" };
|
|
2966
3560
|
}
|
|
2967
3561
|
});
|
|
2968
3562
|
|
|
3563
|
+
// src/claims.ts
|
|
3564
|
+
var claims_exports = {};
|
|
3565
|
+
__export(claims_exports, {
|
|
3566
|
+
acceptedPRRate: () => acceptedPRRate,
|
|
3567
|
+
findClaim: () => findClaim,
|
|
3568
|
+
listClaims: () => listClaims,
|
|
3569
|
+
readClaims: () => readClaims,
|
|
3570
|
+
recordClaim: () => recordClaim,
|
|
3571
|
+
removeClaim: () => removeClaim,
|
|
3572
|
+
updateClaim: () => updateClaim
|
|
3573
|
+
});
|
|
3574
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, renameSync, existsSync as existsSync4 } from "fs";
|
|
3575
|
+
import { join as join6 } from "path";
|
|
3576
|
+
import { homedir as homedir5 } from "os";
|
|
3577
|
+
function nowISO() {
|
|
3578
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3579
|
+
}
|
|
3580
|
+
function readClaims() {
|
|
3581
|
+
try {
|
|
3582
|
+
if (!existsSync4(CLAIMS_FILE)) return [];
|
|
3583
|
+
const data = JSON.parse(readFileSync6(CLAIMS_FILE, "utf8"));
|
|
3584
|
+
return Array.isArray(data?.claims) ? data.claims : [];
|
|
3585
|
+
} catch {
|
|
3586
|
+
return [];
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
function writeClaims(claims) {
|
|
3590
|
+
mkdirSync5(TERMINALHIRE_DIR5, { recursive: true });
|
|
3591
|
+
const tmp = `${CLAIMS_FILE}.tmp`;
|
|
3592
|
+
const payload = { claims };
|
|
3593
|
+
writeFileSync5(tmp, JSON.stringify(payload, null, 2), "utf8");
|
|
3594
|
+
renameSync(tmp, CLAIMS_FILE);
|
|
3595
|
+
}
|
|
3596
|
+
function findClaim(id) {
|
|
3597
|
+
return readClaims().find((c) => c.id === id) ?? null;
|
|
3598
|
+
}
|
|
3599
|
+
function listClaims(opts = {}) {
|
|
3600
|
+
const claims = readClaims();
|
|
3601
|
+
if (!opts.active) return claims;
|
|
3602
|
+
return claims.filter((c) => !TERMINAL_STATES.has(c.state));
|
|
3603
|
+
}
|
|
3604
|
+
function recordClaim(rec) {
|
|
3605
|
+
const claims = readClaims();
|
|
3606
|
+
if (claims.some((c) => c.id === rec.id)) {
|
|
3607
|
+
throw new Error(
|
|
3608
|
+
`claim already exists for '${rec.id}' \u2014 run 'terminalhire claim status ${rec.id}' or 'terminalhire claim release ${rec.id}'`
|
|
3609
|
+
);
|
|
3610
|
+
}
|
|
3611
|
+
const ts = nowISO();
|
|
3612
|
+
const claim = {
|
|
3613
|
+
...rec,
|
|
3614
|
+
state: "claimed",
|
|
3615
|
+
worktreePath: null,
|
|
3616
|
+
branch: null,
|
|
3617
|
+
prUrl: null,
|
|
3618
|
+
review: null,
|
|
3619
|
+
claimedAt: ts,
|
|
3620
|
+
updatedAt: ts
|
|
3621
|
+
};
|
|
3622
|
+
claims.push(claim);
|
|
3623
|
+
writeClaims(claims);
|
|
3624
|
+
return claim;
|
|
3625
|
+
}
|
|
3626
|
+
function updateClaim(id, patch) {
|
|
3627
|
+
const claims = readClaims();
|
|
3628
|
+
const idx = claims.findIndex((c) => c.id === id);
|
|
3629
|
+
if (idx === -1) return null;
|
|
3630
|
+
claims[idx] = { ...claims[idx], ...patch, updatedAt: nowISO() };
|
|
3631
|
+
writeClaims(claims);
|
|
3632
|
+
return claims[idx];
|
|
3633
|
+
}
|
|
3634
|
+
function removeClaim(id) {
|
|
3635
|
+
const claims = readClaims();
|
|
3636
|
+
const next = claims.filter((c) => c.id !== id);
|
|
3637
|
+
if (next.length === claims.length) return false;
|
|
3638
|
+
writeClaims(next);
|
|
3639
|
+
return true;
|
|
3640
|
+
}
|
|
3641
|
+
function acceptedPRRate(claims = readClaims()) {
|
|
3642
|
+
const total = claims.length;
|
|
3643
|
+
const merged = claims.filter((c) => c.state === "merged").length;
|
|
3644
|
+
return { merged, total, rate: total === 0 ? 0 : merged / total };
|
|
3645
|
+
}
|
|
3646
|
+
var TERMINALHIRE_DIR5, CLAIMS_FILE, TERMINAL_STATES;
|
|
3647
|
+
var init_claims = __esm({
|
|
3648
|
+
"src/claims.ts"() {
|
|
3649
|
+
"use strict";
|
|
3650
|
+
TERMINALHIRE_DIR5 = join6(homedir5(), ".terminalhire");
|
|
3651
|
+
CLAIMS_FILE = join6(TERMINALHIRE_DIR5, "claims.json");
|
|
3652
|
+
TERMINAL_STATES = /* @__PURE__ */ new Set(["merged", "abandoned"]);
|
|
3653
|
+
}
|
|
3654
|
+
});
|
|
3655
|
+
|
|
3656
|
+
// bin/jpi-claim.js
|
|
3657
|
+
var jpi_claim_exports = {};
|
|
3658
|
+
__export(jpi_claim_exports, {
|
|
3659
|
+
run: () => run4
|
|
3660
|
+
});
|
|
3661
|
+
import { readFileSync as readFileSync7, existsSync as existsSync5 } from "fs";
|
|
3662
|
+
import { join as join7 } from "path";
|
|
3663
|
+
import { homedir as homedir6 } from "os";
|
|
3664
|
+
function findBountyInCache(bountyId) {
|
|
3665
|
+
try {
|
|
3666
|
+
if (!existsSync5(INDEX_CACHE_FILE3)) return null;
|
|
3667
|
+
const entry = JSON.parse(readFileSync7(INDEX_CACHE_FILE3, "utf8"));
|
|
3668
|
+
const jobs = entry?.index?.jobs ?? [];
|
|
3669
|
+
const job = jobs.find((j) => j.id === bountyId && j.source === "bounty");
|
|
3670
|
+
return job ?? null;
|
|
3671
|
+
} catch {
|
|
3672
|
+
return null;
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
function parseGitHubUrl(url) {
|
|
3676
|
+
const m = String(url ?? "").match(/github\.com\/([^/]+)\/([^/]+)\/(?:issues|pull)\/(\d+)/);
|
|
3677
|
+
if (!m) return null;
|
|
3678
|
+
return { owner: m[1], repo: m[2], number: parseInt(m[3], 10), repoFullName: `${m[1]}/${m[2]}` };
|
|
3679
|
+
}
|
|
3680
|
+
async function countOpenPRsReferencingIssue(repoFullName, issueNumber) {
|
|
3681
|
+
try {
|
|
3682
|
+
const res = await fetch(`${GH_API}/repos/${repoFullName}/pulls?state=open&per_page=100`, {
|
|
3683
|
+
headers: GH_HEADERS,
|
|
3684
|
+
signal: AbortSignal.timeout(1e4)
|
|
3685
|
+
});
|
|
3686
|
+
if (!res.ok) return null;
|
|
3687
|
+
const prs = await res.json();
|
|
3688
|
+
if (!Array.isArray(prs)) return null;
|
|
3689
|
+
if (prs.length === 100) return null;
|
|
3690
|
+
const needle = new RegExp(`#${issueNumber}\\b`);
|
|
3691
|
+
return prs.filter((p) => needle.test(`${p.title ?? ""}
|
|
3692
|
+
${p.body ?? ""}`)).length;
|
|
3693
|
+
} catch {
|
|
3694
|
+
return null;
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
async function fetchIssueState2(repoFullName, issueNumber) {
|
|
3698
|
+
try {
|
|
3699
|
+
const res = await fetch(`${GH_API}/repos/${repoFullName}/issues/${issueNumber}`, {
|
|
3700
|
+
headers: GH_HEADERS,
|
|
3701
|
+
signal: AbortSignal.timeout(1e4)
|
|
3702
|
+
});
|
|
3703
|
+
if (!res.ok) return null;
|
|
3704
|
+
const issue = await res.json();
|
|
3705
|
+
return issue.state === "open" ? "open" : issue.state === "closed" ? "closed" : null;
|
|
3706
|
+
} catch {
|
|
3707
|
+
return null;
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
async function pollPR(prUrl) {
|
|
3711
|
+
const p = parseGitHubUrl(prUrl);
|
|
3712
|
+
if (!p) return null;
|
|
3713
|
+
try {
|
|
3714
|
+
const res = await fetch(`${GH_API}/repos/${p.repoFullName}/pulls/${p.number}`, {
|
|
3715
|
+
headers: GH_HEADERS,
|
|
3716
|
+
signal: AbortSignal.timeout(1e4)
|
|
3717
|
+
});
|
|
3718
|
+
if (!res.ok) return null;
|
|
3719
|
+
const pr = await res.json();
|
|
3720
|
+
return { merged: pr.merged === true, state: pr.state };
|
|
3721
|
+
} catch {
|
|
3722
|
+
return null;
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
function fmtAmount(a) {
|
|
3726
|
+
return a != null ? "$" + a.toLocaleString() : "$\u2014";
|
|
3727
|
+
}
|
|
3728
|
+
function printMetric(rate) {
|
|
3729
|
+
const pct = Math.round(rate.rate * 100);
|
|
3730
|
+
console.log(`
|
|
3731
|
+
\u{1F4CA} Accepted-PR rate: ${rate.merged}/${rate.total} claims merged (${pct}%)`);
|
|
3732
|
+
}
|
|
3733
|
+
async function cmdRecord(arg) {
|
|
3734
|
+
const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
|
|
3735
|
+
if (!arg) {
|
|
3736
|
+
console.error("Usage: terminalhire claim record <bountyId|issueUrl>");
|
|
3737
|
+
console.error(" Run `terminalhire bounties` first to populate the local index cache,");
|
|
3738
|
+
console.error(" then pass the id shown in its output \u2014 or pass a GitHub issue URL directly.");
|
|
3739
|
+
process.exit(1);
|
|
3740
|
+
}
|
|
3741
|
+
let bountyId, title, repoFullName, issueUrl, amountUSD;
|
|
3742
|
+
const job = findBountyInCache(arg);
|
|
3743
|
+
if (job) {
|
|
3744
|
+
const b = job.bounty ?? {};
|
|
3745
|
+
bountyId = job.id;
|
|
3746
|
+
title = job.title;
|
|
3747
|
+
repoFullName = b.repoFullName ?? job.company ?? "";
|
|
3748
|
+
issueUrl = b.claimUrl ?? job.url ?? "";
|
|
3749
|
+
amountUSD = b.amountUSD ?? null;
|
|
3750
|
+
} else {
|
|
3751
|
+
const parsed = parseGitHubUrl(arg);
|
|
3752
|
+
if (!parsed) {
|
|
3753
|
+
console.error(`terminalhire claim: '${arg}' is not in the index cache and is not a GitHub issue URL.`);
|
|
3754
|
+
console.error(" Run `terminalhire bounties` to populate the cache, or pass a full issue URL.");
|
|
3755
|
+
process.exit(1);
|
|
3756
|
+
}
|
|
3757
|
+
bountyId = `gh:${parsed.repoFullName}#${parsed.number}`;
|
|
3758
|
+
title = `${parsed.repoFullName}#${parsed.number}`;
|
|
3759
|
+
repoFullName = parsed.repoFullName;
|
|
3760
|
+
issueUrl = arg;
|
|
3761
|
+
amountUSD = null;
|
|
3762
|
+
}
|
|
3763
|
+
const ghIssue = parseGitHubUrl(issueUrl);
|
|
3764
|
+
const [issueState, openPRs] = ghIssue ? await Promise.all([
|
|
3765
|
+
fetchIssueState2(repoFullName, ghIssue.number),
|
|
3766
|
+
countOpenPRsReferencingIssue(repoFullName, ghIssue.number)
|
|
3767
|
+
// Guardrail #5
|
|
3768
|
+
]) : [null, null];
|
|
3769
|
+
if (issueState === "closed") {
|
|
3770
|
+
console.error(
|
|
3771
|
+
`terminalhire claim: ${repoFullName}#${ghIssue.number} is CLOSED \u2014 not claimable.
|
|
3772
|
+
The bounty index drops closed issues; this one is likely a stale cache entry.
|
|
3773
|
+
Run \`terminalhire bounties\` for the current open pool.`
|
|
3774
|
+
);
|
|
3775
|
+
process.exit(1);
|
|
3776
|
+
}
|
|
3777
|
+
let claim;
|
|
3778
|
+
try {
|
|
3779
|
+
claim = claims.recordClaim({ id: bountyId, bountyId, title, repoFullName, issueUrl, amountUSD, openPRsAtClaim: openPRs });
|
|
3780
|
+
} catch (err) {
|
|
3781
|
+
console.error(`terminalhire claim: ${err.message ?? err}`);
|
|
3782
|
+
process.exit(1);
|
|
3783
|
+
}
|
|
3784
|
+
console.log(`
|
|
3785
|
+
\u2713 Claimed: ${claim.title}`);
|
|
3786
|
+
console.log(` id: ${claim.id}`);
|
|
3787
|
+
console.log(` repo: ${claim.repoFullName}`);
|
|
3788
|
+
console.log(` amount: ${fmtAmount(claim.amountUSD)}`);
|
|
3789
|
+
console.log(` issue: ${claim.issueUrl}`);
|
|
3790
|
+
if (openPRs == null) {
|
|
3791
|
+
console.log(" open PRs: unknown (GitHub read unavailable \u2014 check the issue manually before working)");
|
|
3792
|
+
} else if (openPRs > 0) {
|
|
3793
|
+
console.log(` \u26A0 open PRs referencing this issue: ${openPRs} \u2014 someone may already be on it. Check before working.`);
|
|
3794
|
+
} else {
|
|
3795
|
+
console.log(" open PRs referencing this issue: 0");
|
|
3796
|
+
}
|
|
3797
|
+
console.log("\n Executor constraints (enforce when spawning the background agent):");
|
|
3798
|
+
console.log(" \u2022 work in an ISOLATED git worktree; scrub the subprocess env (no token/profile inheritance)");
|
|
3799
|
+
console.log(" \u2022 MUST NOT `git push` or `gh pr` \u2014 pushing happens only via `terminalhire submit`");
|
|
3800
|
+
console.log(" \u2022 clone + static analysis + patch only; NO test/build execution without explicit approval");
|
|
3801
|
+
console.log(" \u2022 no access to ~/.terminalhire (the executor never needs your profile)");
|
|
3802
|
+
console.log("\n Next: do the work, then `terminalhire claim update " + claim.id + " <state>` as you progress.");
|
|
3803
|
+
}
|
|
3804
|
+
async function cmdList(active) {
|
|
3805
|
+
const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
|
|
3806
|
+
const list = claims.listClaims({ active });
|
|
3807
|
+
if (list.length === 0) {
|
|
3808
|
+
console.log(active ? "No active claims." : "No claims yet. Use `terminalhire claim record <bountyId>`.");
|
|
3809
|
+
return;
|
|
3810
|
+
}
|
|
3811
|
+
console.log(`
|
|
3812
|
+
${list.length} ${active ? "active " : ""}claim${list.length === 1 ? "" : "s"}:
|
|
3813
|
+
`);
|
|
3814
|
+
for (const c of list) {
|
|
3815
|
+
const pr = c.prUrl ? ` \xB7 ${c.prUrl}` : "";
|
|
3816
|
+
console.log(` [${c.state}] ${fmtAmount(c.amountUSD)} \xB7 ${c.title}`);
|
|
3817
|
+
console.log(` id: ${c.id}${pr}`);
|
|
3818
|
+
}
|
|
3819
|
+
printMetric(claims.acceptedPRRate());
|
|
3820
|
+
}
|
|
3821
|
+
async function cmdStatus(id) {
|
|
3822
|
+
const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
|
|
3823
|
+
const targets = id ? [claims.findClaim(id)].filter(Boolean) : claims.listClaims();
|
|
3824
|
+
if (targets.length === 0) {
|
|
3825
|
+
console.log(id ? `No claim with id '${id}'.` : "No claims to poll.");
|
|
3826
|
+
return;
|
|
3827
|
+
}
|
|
3828
|
+
let polled = 0;
|
|
3829
|
+
for (const c of targets) {
|
|
3830
|
+
if (!c.prUrl) continue;
|
|
3831
|
+
const res = await pollPR(c.prUrl);
|
|
3832
|
+
if (!res) {
|
|
3833
|
+
console.log(` ? ${c.title} \u2014 could not read PR state (${c.prUrl})`);
|
|
3834
|
+
continue;
|
|
3835
|
+
}
|
|
3836
|
+
polled++;
|
|
3837
|
+
let next = c.state;
|
|
3838
|
+
if (res.merged) next = "merged";
|
|
3839
|
+
else if (res.state === "closed") next = "abandoned";
|
|
3840
|
+
else next = "submitted";
|
|
3841
|
+
const ORDER = ["claimed", "working", "in-review", "ready", "submitted", "merged", "abandoned"];
|
|
3842
|
+
if (next !== c.state && ORDER.indexOf(next) > ORDER.indexOf(c.state)) {
|
|
3843
|
+
claims.updateClaim(c.id, { state: next });
|
|
3844
|
+
}
|
|
3845
|
+
const mark = res.merged ? "\u2713 merged" : res.state === "closed" ? "\u2717 closed (unmerged)" : "\u2026 open";
|
|
3846
|
+
console.log(` ${mark} \u2014 ${c.title} (${c.prUrl})`);
|
|
3847
|
+
}
|
|
3848
|
+
if (polled === 0) console.log(" No submitted claims with a PR URL yet. Set one via `claim update <id> submitted` after `submit`.");
|
|
3849
|
+
printMetric(claims.acceptedPRRate());
|
|
3850
|
+
}
|
|
3851
|
+
async function cmdUpdate(id, state, prUrl) {
|
|
3852
|
+
const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
|
|
3853
|
+
const VALID = ["claimed", "working", "in-review", "ready", "submitted", "merged", "abandoned"];
|
|
3854
|
+
if (!id || !state || !VALID.includes(state)) {
|
|
3855
|
+
console.error("Usage: terminalhire claim update <id> <state> [prUrl]");
|
|
3856
|
+
console.error(" state: " + VALID.join(" | "));
|
|
3857
|
+
console.error(" prUrl: attach the source PR URL (so `claim status` can poll its merge state)");
|
|
3858
|
+
process.exit(1);
|
|
3859
|
+
}
|
|
3860
|
+
const patch = { state };
|
|
3861
|
+
if (prUrl) {
|
|
3862
|
+
if (!parseGitHubUrl(prUrl)) {
|
|
3863
|
+
console.error(`terminalhire claim: '${prUrl}' is not a GitHub PR URL.`);
|
|
3864
|
+
process.exit(1);
|
|
3865
|
+
}
|
|
3866
|
+
patch.prUrl = prUrl;
|
|
3867
|
+
}
|
|
3868
|
+
const updated = claims.updateClaim(id, patch);
|
|
3869
|
+
if (!updated) {
|
|
3870
|
+
console.error(`terminalhire claim: no claim with id '${id}'.`);
|
|
3871
|
+
process.exit(1);
|
|
3872
|
+
}
|
|
3873
|
+
console.log(`Updated ${id} \u2192 ${state}${prUrl ? ` (PR: ${prUrl})` : ""}`);
|
|
3874
|
+
}
|
|
3875
|
+
async function cmdRelease(id) {
|
|
3876
|
+
const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
|
|
3877
|
+
if (!id) {
|
|
3878
|
+
console.error("Usage: terminalhire claim release <id>");
|
|
3879
|
+
process.exit(1);
|
|
3880
|
+
}
|
|
3881
|
+
const removed = claims.removeClaim(id);
|
|
3882
|
+
console.log(removed ? `Released claim: ${id}` : `terminalhire claim: no claim with id '${id}'.`);
|
|
3883
|
+
if (!removed) process.exit(1);
|
|
3884
|
+
}
|
|
3885
|
+
async function run4() {
|
|
3886
|
+
const verb = process.argv[2];
|
|
3887
|
+
const rest = process.argv.slice(3).filter((a) => !a.startsWith("--"));
|
|
3888
|
+
const active = process.argv.includes("--active");
|
|
3889
|
+
try {
|
|
3890
|
+
switch (verb) {
|
|
3891
|
+
case "record":
|
|
3892
|
+
await cmdRecord(rest[0]);
|
|
3893
|
+
break;
|
|
3894
|
+
case "list":
|
|
3895
|
+
await cmdList(active);
|
|
3896
|
+
break;
|
|
3897
|
+
case "status":
|
|
3898
|
+
await cmdStatus(rest[0]);
|
|
3899
|
+
break;
|
|
3900
|
+
case "update":
|
|
3901
|
+
await cmdUpdate(rest[0], rest[1], rest[2]);
|
|
3902
|
+
break;
|
|
3903
|
+
case "release":
|
|
3904
|
+
await cmdRelease(rest[0]);
|
|
3905
|
+
break;
|
|
3906
|
+
default:
|
|
3907
|
+
console.error(`terminalhire claim: unknown verb '${verb ?? ""}'. Expected: record | list | status | update | release`);
|
|
3908
|
+
process.exit(1);
|
|
3909
|
+
}
|
|
3910
|
+
} catch (err) {
|
|
3911
|
+
console.error("terminalhire claim error:", err.message ?? err);
|
|
3912
|
+
process.exit(1);
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
var TERMINALHIRE_DIR6, INDEX_CACHE_FILE3, GH_API, GH_HEADERS;
|
|
3916
|
+
var init_jpi_claim = __esm({
|
|
3917
|
+
"bin/jpi-claim.js"() {
|
|
3918
|
+
"use strict";
|
|
3919
|
+
TERMINALHIRE_DIR6 = join7(homedir6(), ".terminalhire");
|
|
3920
|
+
INDEX_CACHE_FILE3 = join7(TERMINALHIRE_DIR6, "index-cache.json");
|
|
3921
|
+
GH_API = "https://api.github.com";
|
|
3922
|
+
GH_HEADERS = { "User-Agent": "terminalhire-claim", Accept: "application/vnd.github+json" };
|
|
3923
|
+
}
|
|
3924
|
+
});
|
|
3925
|
+
|
|
2969
3926
|
// bin/jpi-profile.js
|
|
2970
3927
|
var jpi_profile_exports = {};
|
|
2971
3928
|
__export(jpi_profile_exports, {
|
|
2972
|
-
run: () =>
|
|
3929
|
+
run: () => run5
|
|
2973
3930
|
});
|
|
2974
3931
|
import { createInterface as createInterface3 } from "readline";
|
|
2975
3932
|
function prompt3(question) {
|
|
@@ -2981,7 +3938,7 @@ function prompt3(question) {
|
|
|
2981
3938
|
});
|
|
2982
3939
|
});
|
|
2983
3940
|
}
|
|
2984
|
-
async function
|
|
3941
|
+
async function run5() {
|
|
2985
3942
|
const { readProfile: readProfile2, writeProfile: writeProfile2, deleteProfile: deleteProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
|
|
2986
3943
|
const args3 = process.argv.slice(2);
|
|
2987
3944
|
if (args3.includes("--show")) {
|
|
@@ -3054,9 +4011,9 @@ var signal_exports = {};
|
|
|
3054
4011
|
__export(signal_exports, {
|
|
3055
4012
|
extractFingerprint: () => extractFingerprint
|
|
3056
4013
|
});
|
|
3057
|
-
import { readFileSync as
|
|
4014
|
+
import { readFileSync as readFileSync8, readdirSync } from "fs";
|
|
3058
4015
|
import { execFileSync } from "child_process";
|
|
3059
|
-
import { join as
|
|
4016
|
+
import { join as join8 } from "path";
|
|
3060
4017
|
function safeGit(args3, cwd) {
|
|
3061
4018
|
try {
|
|
3062
4019
|
return execFileSync("git", ["-C", cwd, ...args3], {
|
|
@@ -3084,20 +4041,20 @@ function isEmployerContext(cwd) {
|
|
|
3084
4041
|
}
|
|
3085
4042
|
function readJsonSafe(path) {
|
|
3086
4043
|
try {
|
|
3087
|
-
return JSON.parse(
|
|
4044
|
+
return JSON.parse(readFileSync8(path, "utf8"));
|
|
3088
4045
|
} catch {
|
|
3089
4046
|
return null;
|
|
3090
4047
|
}
|
|
3091
4048
|
}
|
|
3092
4049
|
function readFileSafe(path) {
|
|
3093
4050
|
try {
|
|
3094
|
-
return
|
|
4051
|
+
return readFileSync8(path, "utf8");
|
|
3095
4052
|
} catch {
|
|
3096
4053
|
return "";
|
|
3097
4054
|
}
|
|
3098
4055
|
}
|
|
3099
4056
|
function tokensFromPackageJson(cwd) {
|
|
3100
|
-
const pkg = readJsonSafe(
|
|
4057
|
+
const pkg = readJsonSafe(join8(cwd, "package.json"));
|
|
3101
4058
|
if (!pkg || typeof pkg !== "object") return [];
|
|
3102
4059
|
const p = pkg;
|
|
3103
4060
|
const deps = {
|
|
@@ -3111,9 +4068,9 @@ function workspaceMemberDirs(cwd) {
|
|
|
3111
4068
|
const dirs = [cwd];
|
|
3112
4069
|
for (const group of ["apps", "packages"]) {
|
|
3113
4070
|
try {
|
|
3114
|
-
const groupDir =
|
|
4071
|
+
const groupDir = join8(cwd, group);
|
|
3115
4072
|
for (const e of readdirSync(groupDir, { withFileTypes: true })) {
|
|
3116
|
-
if (e.isDirectory() && !e.isSymbolicLink()) dirs.push(
|
|
4073
|
+
if (e.isDirectory() && !e.isSymbolicLink()) dirs.push(join8(groupDir, e.name));
|
|
3117
4074
|
}
|
|
3118
4075
|
} catch {
|
|
3119
4076
|
}
|
|
@@ -3121,18 +4078,18 @@ function workspaceMemberDirs(cwd) {
|
|
|
3121
4078
|
return dirs;
|
|
3122
4079
|
}
|
|
3123
4080
|
function tokensFromRequirementsTxt(cwd) {
|
|
3124
|
-
const content = readFileSafe(
|
|
4081
|
+
const content = readFileSafe(join8(cwd, "requirements.txt"));
|
|
3125
4082
|
if (!content) return [];
|
|
3126
4083
|
return content.split("\n").map((l) => l.trim().split(/[>=<!\[;]/)[0].trim().toLowerCase()).filter(Boolean);
|
|
3127
4084
|
}
|
|
3128
4085
|
function tokensFromGoMod(cwd) {
|
|
3129
|
-
const content = readFileSafe(
|
|
4086
|
+
const content = readFileSafe(join8(cwd, "go.mod"));
|
|
3130
4087
|
if (!content) return [];
|
|
3131
4088
|
const requires = Array.from(content.matchAll(/^\s+([^\s]+)\s+v/gm)).map((m) => m[1].split("/").pop() ?? "").filter(Boolean);
|
|
3132
4089
|
return ["go", ...requires];
|
|
3133
4090
|
}
|
|
3134
4091
|
function tokensFromCargoToml(cwd) {
|
|
3135
|
-
const content = readFileSafe(
|
|
4092
|
+
const content = readFileSafe(join8(cwd, "Cargo.toml"));
|
|
3136
4093
|
if (!content) return [];
|
|
3137
4094
|
const deps = [];
|
|
3138
4095
|
let inDeps = false;
|
|
@@ -3153,7 +4110,7 @@ function tokensFromFileExtensions(cwd) {
|
|
|
3153
4110
|
const tokens = [];
|
|
3154
4111
|
const scanDirs = [cwd];
|
|
3155
4112
|
try {
|
|
3156
|
-
const srcDir =
|
|
4113
|
+
const srcDir = join8(cwd, "src");
|
|
3157
4114
|
readdirSync(srcDir);
|
|
3158
4115
|
scanDirs.push(srcDir);
|
|
3159
4116
|
} catch {
|
|
@@ -3314,9 +4271,9 @@ var init_signal = __esm({
|
|
|
3314
4271
|
// bin/jpi-learn.js
|
|
3315
4272
|
var jpi_learn_exports = {};
|
|
3316
4273
|
__export(jpi_learn_exports, {
|
|
3317
|
-
run: () =>
|
|
4274
|
+
run: () => run6
|
|
3318
4275
|
});
|
|
3319
|
-
async function
|
|
4276
|
+
async function run6() {
|
|
3320
4277
|
try {
|
|
3321
4278
|
const args3 = process.argv.slice(2);
|
|
3322
4279
|
const cwdIdx = args3.indexOf("--cwd");
|
|
@@ -3343,7 +4300,7 @@ var init_jpi_learn = __esm({
|
|
|
3343
4300
|
"use strict";
|
|
3344
4301
|
isMain = process.argv[1]?.endsWith("jpi-learn.js") || process.argv[1]?.endsWith("jpi-learn");
|
|
3345
4302
|
if (isMain) {
|
|
3346
|
-
|
|
4303
|
+
run6();
|
|
3347
4304
|
}
|
|
3348
4305
|
}
|
|
3349
4306
|
});
|
|
@@ -3351,23 +4308,23 @@ var init_jpi_learn = __esm({
|
|
|
3351
4308
|
// bin/jpi-config.js
|
|
3352
4309
|
var jpi_config_exports = {};
|
|
3353
4310
|
__export(jpi_config_exports, {
|
|
3354
|
-
run: () =>
|
|
4311
|
+
run: () => run7
|
|
3355
4312
|
});
|
|
3356
|
-
import { readFileSync as
|
|
3357
|
-
import { join as
|
|
3358
|
-
import { homedir as
|
|
4313
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, existsSync as existsSync6 } from "fs";
|
|
4314
|
+
import { join as join9 } from "path";
|
|
4315
|
+
import { homedir as homedir7 } from "os";
|
|
3359
4316
|
function readConfig() {
|
|
3360
4317
|
try {
|
|
3361
|
-
if (!
|
|
3362
|
-
return { ...DEFAULT_CONFIG, ...JSON.parse(
|
|
4318
|
+
if (!existsSync6(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
|
|
4319
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync9(CONFIG_FILE, "utf8")) };
|
|
3363
4320
|
} catch {
|
|
3364
4321
|
return { ...DEFAULT_CONFIG };
|
|
3365
4322
|
}
|
|
3366
4323
|
}
|
|
3367
4324
|
function writeConfig(patch) {
|
|
3368
|
-
|
|
4325
|
+
mkdirSync6(TERMINALHIRE_DIR7, { recursive: true });
|
|
3369
4326
|
const merged = { ...readConfig(), ...patch };
|
|
3370
|
-
|
|
4327
|
+
writeFileSync6(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
3371
4328
|
}
|
|
3372
4329
|
function parseNudgeMode(raw) {
|
|
3373
4330
|
if (raw === "session" || raw === "always") return raw;
|
|
@@ -3375,7 +4332,7 @@ function parseNudgeMode(raw) {
|
|
|
3375
4332
|
if (m && parseInt(m[1], 10) >= 1) return raw;
|
|
3376
4333
|
return null;
|
|
3377
4334
|
}
|
|
3378
|
-
async function
|
|
4335
|
+
async function run7() {
|
|
3379
4336
|
const args3 = process.argv.slice(2);
|
|
3380
4337
|
const filtered = args3[0] === "config" ? args3.slice(1) : args3;
|
|
3381
4338
|
if (filtered.includes("--show") || filtered.length === 0) {
|
|
@@ -3418,12 +4375,12 @@ async function run6() {
|
|
|
3418
4375
|
console.error(" terminalhire config --show");
|
|
3419
4376
|
process.exit(1);
|
|
3420
4377
|
}
|
|
3421
|
-
var
|
|
4378
|
+
var TERMINALHIRE_DIR7, CONFIG_FILE, DEFAULT_CONFIG;
|
|
3422
4379
|
var init_jpi_config = __esm({
|
|
3423
4380
|
"bin/jpi-config.js"() {
|
|
3424
4381
|
"use strict";
|
|
3425
|
-
|
|
3426
|
-
CONFIG_FILE =
|
|
4382
|
+
TERMINALHIRE_DIR7 = join9(homedir7(), ".terminalhire");
|
|
4383
|
+
CONFIG_FILE = join9(TERMINALHIRE_DIR7, "config.json");
|
|
3427
4384
|
DEFAULT_CONFIG = { nudge: "session" };
|
|
3428
4385
|
}
|
|
3429
4386
|
});
|
|
@@ -3446,26 +4403,26 @@ __export(spinner_exports, {
|
|
|
3446
4403
|
readSpinnerConfig: () => readSpinnerConfig
|
|
3447
4404
|
});
|
|
3448
4405
|
import {
|
|
3449
|
-
readFileSync as
|
|
3450
|
-
writeFileSync as
|
|
3451
|
-
existsSync as
|
|
3452
|
-
mkdirSync as
|
|
3453
|
-
renameSync
|
|
4406
|
+
readFileSync as readFileSync10,
|
|
4407
|
+
writeFileSync as writeFileSync7,
|
|
4408
|
+
existsSync as existsSync7,
|
|
4409
|
+
mkdirSync as mkdirSync7,
|
|
4410
|
+
renameSync as renameSync2
|
|
3454
4411
|
} from "fs";
|
|
3455
|
-
import { join as
|
|
3456
|
-
import { homedir as
|
|
4412
|
+
import { join as join10, dirname } from "path";
|
|
4413
|
+
import { homedir as homedir8 } from "os";
|
|
3457
4414
|
function readJson(path, fallback) {
|
|
3458
4415
|
try {
|
|
3459
|
-
return
|
|
4416
|
+
return existsSync7(path) ? JSON.parse(readFileSync10(path, "utf8")) : fallback;
|
|
3460
4417
|
} catch {
|
|
3461
4418
|
return fallback;
|
|
3462
4419
|
}
|
|
3463
4420
|
}
|
|
3464
4421
|
function atomicWriteJson(path, obj) {
|
|
3465
|
-
|
|
4422
|
+
mkdirSync7(dirname(path), { recursive: true });
|
|
3466
4423
|
const tmp = `${path}.tmp-${process.pid}`;
|
|
3467
|
-
|
|
3468
|
-
|
|
4424
|
+
writeFileSync7(tmp, JSON.stringify(obj, null, 2) + "\n", "utf8");
|
|
4425
|
+
renameSync2(tmp, path);
|
|
3469
4426
|
}
|
|
3470
4427
|
function titleCase(s) {
|
|
3471
4428
|
return String(s || "").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
@@ -3548,7 +4505,7 @@ function buildContextVerbs(topMatches, sessionTags) {
|
|
|
3548
4505
|
}
|
|
3549
4506
|
const list = Array.isArray(topMatches) ? topMatches : [];
|
|
3550
4507
|
const hasBounty = list.some((m) => m && m.source === "bounty");
|
|
3551
|
-
if (hasBounty) headers.
|
|
4508
|
+
if (hasBounty) headers.push(`\u2726 Roles + \u{1F48E} paid bounties in your stack \u2014 link below`);
|
|
3552
4509
|
return headers;
|
|
3553
4510
|
}
|
|
3554
4511
|
function buildSpinnerPool(topMatches, max = 6, opts = {}) {
|
|
@@ -3640,8 +4597,8 @@ function buildTips(topMatches, baseUrl, max = 8) {
|
|
|
3640
4597
|
let bi = 0;
|
|
3641
4598
|
let ri = 0;
|
|
3642
4599
|
while (bi < bountyQ.length || ri < roleQ.length) {
|
|
3643
|
-
if (bi < bountyQ.length) ordered.push(bountyQ[bi++]);
|
|
3644
4600
|
if (ri < roleQ.length) ordered.push(roleQ[ri++]);
|
|
4601
|
+
if (bi < bountyQ.length) ordered.push(bountyQ[bi++]);
|
|
3645
4602
|
}
|
|
3646
4603
|
for (const m of ordered) {
|
|
3647
4604
|
if (!m || !m.title || !m.company || !m.id) continue;
|
|
@@ -3716,10 +4673,10 @@ var TH_DIR, CLAUDE_SETTINGS, CONFIG_FILE2, SPINNER_STATE_FILE, SPINNER_DEFAULTS,
|
|
|
3716
4673
|
var init_spinner = __esm({
|
|
3717
4674
|
"bin/spinner.js"() {
|
|
3718
4675
|
"use strict";
|
|
3719
|
-
TH_DIR = process.env["TERMINALHIRE_DIR"] ||
|
|
3720
|
-
CLAUDE_SETTINGS = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] ||
|
|
3721
|
-
CONFIG_FILE2 =
|
|
3722
|
-
SPINNER_STATE_FILE =
|
|
4676
|
+
TH_DIR = process.env["TERMINALHIRE_DIR"] || join10(homedir8(), ".terminalhire");
|
|
4677
|
+
CLAUDE_SETTINGS = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join10(homedir8(), ".claude", "settings.json");
|
|
4678
|
+
CONFIG_FILE2 = join10(TH_DIR, "config.json");
|
|
4679
|
+
SPINNER_STATE_FILE = join10(TH_DIR, "spinner-state.json");
|
|
3723
4680
|
SPINNER_DEFAULTS = { enabled: false, mode: "append", max: 6, frequency: "sometimes" };
|
|
3724
4681
|
VERB_INTROS = ["Matched:", "You\u2019d fit:", "Worth a look:", "On your radar:", "Fits your stack:"];
|
|
3725
4682
|
}
|
|
@@ -3728,32 +4685,32 @@ var init_spinner = __esm({
|
|
|
3728
4685
|
// bin/jpi-spinner.js
|
|
3729
4686
|
var jpi_spinner_exports = {};
|
|
3730
4687
|
__export(jpi_spinner_exports, {
|
|
3731
|
-
run: () =>
|
|
4688
|
+
run: () => run8
|
|
3732
4689
|
});
|
|
3733
4690
|
import {
|
|
3734
|
-
readFileSync as
|
|
3735
|
-
writeFileSync as
|
|
4691
|
+
readFileSync as readFileSync11,
|
|
4692
|
+
writeFileSync as writeFileSync8,
|
|
3736
4693
|
copyFileSync,
|
|
3737
|
-
existsSync as
|
|
3738
|
-
mkdirSync as
|
|
4694
|
+
existsSync as existsSync8,
|
|
4695
|
+
mkdirSync as mkdirSync8
|
|
3739
4696
|
} from "fs";
|
|
3740
|
-
import { join as
|
|
3741
|
-
import { homedir as
|
|
4697
|
+
import { join as join11 } from "path";
|
|
4698
|
+
import { homedir as homedir9 } from "os";
|
|
3742
4699
|
import { createInterface as createInterface4 } from "readline";
|
|
3743
4700
|
function readConfig2() {
|
|
3744
4701
|
try {
|
|
3745
|
-
return
|
|
4702
|
+
return existsSync8(CONFIG_FILE3) ? JSON.parse(readFileSync11(CONFIG_FILE3, "utf8")) : {};
|
|
3746
4703
|
} catch {
|
|
3747
4704
|
return {};
|
|
3748
4705
|
}
|
|
3749
4706
|
}
|
|
3750
4707
|
function writeConfig2(patch) {
|
|
3751
|
-
|
|
4708
|
+
mkdirSync8(TH_DIR2, { recursive: true });
|
|
3752
4709
|
const merged = { ...readConfig2(), ...patch };
|
|
3753
|
-
|
|
4710
|
+
writeFileSync8(CONFIG_FILE3, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
3754
4711
|
}
|
|
3755
4712
|
function backupSettings() {
|
|
3756
|
-
if (!
|
|
4713
|
+
if (!existsSync8(SETTINGS_PATH)) return null;
|
|
3757
4714
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3758
4715
|
const backupPath = `${SETTINGS_PATH}.terminalhire-backup-${ts}`;
|
|
3759
4716
|
copyFileSync(SETTINGS_PATH, backupPath);
|
|
@@ -3770,13 +4727,13 @@ function ask(question) {
|
|
|
3770
4727
|
}
|
|
3771
4728
|
function readTopMatches() {
|
|
3772
4729
|
try {
|
|
3773
|
-
const c = JSON.parse(
|
|
4730
|
+
const c = JSON.parse(readFileSync11(CACHE_FILE, "utf8"));
|
|
3774
4731
|
return Array.isArray(c.topMatches) ? c.topMatches : [];
|
|
3775
4732
|
} catch {
|
|
3776
4733
|
return [];
|
|
3777
4734
|
}
|
|
3778
4735
|
}
|
|
3779
|
-
async function
|
|
4736
|
+
async function run8() {
|
|
3780
4737
|
const args3 = process.argv.slice(2).filter((a) => a !== "spinner");
|
|
3781
4738
|
const has = (f) => args3.includes(f);
|
|
3782
4739
|
const val = (f) => {
|
|
@@ -3913,21 +4870,21 @@ var init_jpi_spinner = __esm({
|
|
|
3913
4870
|
"bin/jpi-spinner.js"() {
|
|
3914
4871
|
"use strict";
|
|
3915
4872
|
init_spinner();
|
|
3916
|
-
TH_DIR2 = process.env["TERMINALHIRE_DIR"] ||
|
|
3917
|
-
CONFIG_FILE3 =
|
|
3918
|
-
SETTINGS_PATH = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] ||
|
|
3919
|
-
CACHE_FILE =
|
|
4873
|
+
TH_DIR2 = process.env["TERMINALHIRE_DIR"] || join11(homedir9(), ".terminalhire");
|
|
4874
|
+
CONFIG_FILE3 = join11(TH_DIR2, "config.json");
|
|
4875
|
+
SETTINGS_PATH = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join11(homedir9(), ".claude", "settings.json");
|
|
4876
|
+
CACHE_FILE = join11(TH_DIR2, "index-cache.json");
|
|
3920
4877
|
}
|
|
3921
4878
|
});
|
|
3922
4879
|
|
|
3923
4880
|
// bin/jpi-sync.js
|
|
3924
4881
|
var jpi_sync_exports = {};
|
|
3925
4882
|
__export(jpi_sync_exports, {
|
|
3926
|
-
run: () =>
|
|
4883
|
+
run: () => run9
|
|
3927
4884
|
});
|
|
3928
|
-
import { readFileSync as
|
|
3929
|
-
import { join as
|
|
3930
|
-
import { homedir as
|
|
4885
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, existsSync as existsSync9, rmSync as rmSync2 } from "fs";
|
|
4886
|
+
import { join as join12 } from "path";
|
|
4887
|
+
import { homedir as homedir10, hostname as osHostname } from "os";
|
|
3931
4888
|
import { createInterface as createInterface5 } from "readline";
|
|
3932
4889
|
import { spawn } from "child_process";
|
|
3933
4890
|
function ask2(question) {
|
|
@@ -3941,14 +4898,14 @@ function ask2(question) {
|
|
|
3941
4898
|
}
|
|
3942
4899
|
function readMarker() {
|
|
3943
4900
|
try {
|
|
3944
|
-
return
|
|
4901
|
+
return existsSync9(TIER1_MARKER) ? JSON.parse(readFileSync12(TIER1_MARKER, "utf8")) : null;
|
|
3945
4902
|
} catch {
|
|
3946
4903
|
return null;
|
|
3947
4904
|
}
|
|
3948
4905
|
}
|
|
3949
4906
|
function writeMarker(marker) {
|
|
3950
|
-
|
|
3951
|
-
|
|
4907
|
+
mkdirSync9(TH_DIR3, { recursive: true });
|
|
4908
|
+
writeFileSync9(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
3952
4909
|
}
|
|
3953
4910
|
function clearMarker() {
|
|
3954
4911
|
try {
|
|
@@ -4257,7 +5214,7 @@ async function runDelete() {
|
|
|
4257
5214
|
clearMarker();
|
|
4258
5215
|
console.log("\n Synced profile deleted and local marker cleared.\n");
|
|
4259
5216
|
}
|
|
4260
|
-
async function
|
|
5217
|
+
async function run9() {
|
|
4261
5218
|
const args3 = process.argv.slice(2).filter((a) => a !== "sync");
|
|
4262
5219
|
const has = (f) => args3.includes(f);
|
|
4263
5220
|
if (has("--push") || has("--enable")) {
|
|
@@ -4287,8 +5244,8 @@ var TH_DIR3, TIER1_MARKER, API_URL3, SYNC_BASE, POLL_INTERVAL_MS, POLL_TIMEOUT_M
|
|
|
4287
5244
|
var init_jpi_sync = __esm({
|
|
4288
5245
|
"bin/jpi-sync.js"() {
|
|
4289
5246
|
"use strict";
|
|
4290
|
-
TH_DIR3 = process.env["TERMINALHIRE_DIR"] ||
|
|
4291
|
-
TIER1_MARKER =
|
|
5247
|
+
TH_DIR3 = process.env["TERMINALHIRE_DIR"] || join12(homedir10(), ".terminalhire");
|
|
5248
|
+
TIER1_MARKER = join12(TH_DIR3, "tier1.json");
|
|
4292
5249
|
API_URL3 = process.env["TERMINALHIRE_API_URL"] || process.env["JPI_API_URL"] || "https://terminalhire.com";
|
|
4293
5250
|
SYNC_BASE = "https://www.terminalhire.com";
|
|
4294
5251
|
POLL_INTERVAL_MS = 2e3;
|
|
@@ -4300,14 +5257,14 @@ var init_jpi_sync = __esm({
|
|
|
4300
5257
|
// bin/jpi-init.js
|
|
4301
5258
|
var jpi_init_exports = {};
|
|
4302
5259
|
__export(jpi_init_exports, {
|
|
4303
|
-
run: () =>
|
|
5260
|
+
run: () => run10
|
|
4304
5261
|
});
|
|
4305
|
-
import { existsSync as
|
|
4306
|
-
import { join as
|
|
5262
|
+
import { existsSync as existsSync10 } from "fs";
|
|
5263
|
+
import { join as join13, resolve } from "path";
|
|
4307
5264
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4308
5265
|
import { createInterface as createInterface6 } from "readline";
|
|
4309
5266
|
import { spawnSync, spawn as spawn2 } from "child_process";
|
|
4310
|
-
import { homedir as
|
|
5267
|
+
import { homedir as homedir11 } from "os";
|
|
4311
5268
|
function ask3(question) {
|
|
4312
5269
|
const rl = createInterface6({ input: process.stdin, output: process.stdout });
|
|
4313
5270
|
return new Promise((resolve2) => {
|
|
@@ -4318,18 +5275,18 @@ function ask3(question) {
|
|
|
4318
5275
|
});
|
|
4319
5276
|
}
|
|
4320
5277
|
function resolveScript(name) {
|
|
4321
|
-
const distPath = resolve(
|
|
4322
|
-
const legacyPath = resolve(
|
|
4323
|
-
return
|
|
5278
|
+
const distPath = resolve(join13(__dirname2, "..", "..", "dist", "bin", `${name}.js`));
|
|
5279
|
+
const legacyPath = resolve(join13(__dirname2, `${name}.js`));
|
|
5280
|
+
return existsSync10(distPath) ? distPath : legacyPath;
|
|
4324
5281
|
}
|
|
4325
5282
|
function resolveInstallJs() {
|
|
4326
|
-
const fromDist = resolve(
|
|
4327
|
-
const fromBin = resolve(
|
|
4328
|
-
if (
|
|
4329
|
-
if (
|
|
5283
|
+
const fromDist = resolve(join13(__dirname2, "..", "..", "install.js"));
|
|
5284
|
+
const fromBin = resolve(join13(__dirname2, "..", "install.js"));
|
|
5285
|
+
if (existsSync10(fromDist)) return fromDist;
|
|
5286
|
+
if (existsSync10(fromBin)) return fromBin;
|
|
4330
5287
|
return fromBin;
|
|
4331
5288
|
}
|
|
4332
|
-
async function
|
|
5289
|
+
async function run10() {
|
|
4333
5290
|
console.log("");
|
|
4334
5291
|
console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
4335
5292
|
console.log("\u2502 terminalhire init \u2014 one-command onboarding \u2502");
|
|
@@ -4432,13 +5389,13 @@ var init_jpi_init = __esm({
|
|
|
4432
5389
|
// bin/jpi-refresh.js
|
|
4433
5390
|
var jpi_refresh_exports = {};
|
|
4434
5391
|
__export(jpi_refresh_exports, {
|
|
4435
|
-
run: () =>
|
|
5392
|
+
run: () => run11
|
|
4436
5393
|
});
|
|
4437
|
-
import { readFileSync as
|
|
4438
|
-
import { join as
|
|
4439
|
-
import { homedir as
|
|
5394
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync11, mkdirSync as mkdirSync10 } from "fs";
|
|
5395
|
+
import { join as join14 } from "path";
|
|
5396
|
+
import { homedir as homedir12 } from "os";
|
|
4440
5397
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
4441
|
-
async function
|
|
5398
|
+
async function run11() {
|
|
4442
5399
|
try {
|
|
4443
5400
|
let index;
|
|
4444
5401
|
try {
|
|
@@ -4490,14 +5447,14 @@ async function run10() {
|
|
|
4490
5447
|
}
|
|
4491
5448
|
} catch {
|
|
4492
5449
|
}
|
|
4493
|
-
|
|
5450
|
+
mkdirSync10(TERMINALHIRE_DIR8, { recursive: true });
|
|
4494
5451
|
const cacheEntry = {
|
|
4495
5452
|
ts: Date.now(),
|
|
4496
5453
|
index,
|
|
4497
5454
|
matchCount,
|
|
4498
5455
|
topMatches
|
|
4499
5456
|
};
|
|
4500
|
-
|
|
5457
|
+
writeFileSync10(INDEX_CACHE_FILE4, JSON.stringify(cacheEntry), "utf8");
|
|
4501
5458
|
try {
|
|
4502
5459
|
const {
|
|
4503
5460
|
readSpinnerConfig: readSpinnerConfig2,
|
|
@@ -4541,13 +5498,13 @@ async function run10() {
|
|
|
4541
5498
|
process.exit(1);
|
|
4542
5499
|
}
|
|
4543
5500
|
}
|
|
4544
|
-
var __dirname3,
|
|
5501
|
+
var __dirname3, TERMINALHIRE_DIR8, INDEX_CACHE_FILE4, API_URL4;
|
|
4545
5502
|
var init_jpi_refresh = __esm({
|
|
4546
5503
|
"bin/jpi-refresh.js"() {
|
|
4547
5504
|
"use strict";
|
|
4548
5505
|
__dirname3 = fileURLToPath4(new URL(".", import.meta.url));
|
|
4549
|
-
|
|
4550
|
-
|
|
5506
|
+
TERMINALHIRE_DIR8 = join14(homedir12(), ".terminalhire");
|
|
5507
|
+
INDEX_CACHE_FILE4 = join14(TERMINALHIRE_DIR8, "index-cache.json");
|
|
4551
5508
|
API_URL4 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
|
|
4552
5509
|
}
|
|
4553
5510
|
});
|
|
@@ -4555,16 +5512,16 @@ var init_jpi_refresh = __esm({
|
|
|
4555
5512
|
// bin/jpi-save.js
|
|
4556
5513
|
var jpi_save_exports = {};
|
|
4557
5514
|
__export(jpi_save_exports, {
|
|
4558
|
-
run: () =>
|
|
5515
|
+
run: () => run12
|
|
4559
5516
|
});
|
|
4560
|
-
import { readFileSync as
|
|
4561
|
-
import { join as
|
|
4562
|
-
import { homedir as
|
|
5517
|
+
import { readFileSync as readFileSync14, existsSync as existsSync12 } from "fs";
|
|
5518
|
+
import { join as join15 } from "path";
|
|
5519
|
+
import { homedir as homedir13 } from "os";
|
|
4563
5520
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
4564
5521
|
function findJobInCache(jobId) {
|
|
4565
5522
|
try {
|
|
4566
|
-
if (!
|
|
4567
|
-
const raw =
|
|
5523
|
+
if (!existsSync12(INDEX_CACHE_FILE5)) return null;
|
|
5524
|
+
const raw = readFileSync14(INDEX_CACHE_FILE5, "utf8");
|
|
4568
5525
|
const entry = JSON.parse(raw);
|
|
4569
5526
|
const jobs = entry?.index?.jobs ?? [];
|
|
4570
5527
|
return jobs.find((j) => j.id === jobId) ?? null;
|
|
@@ -4633,7 +5590,7 @@ async function cmdUnsave(jobId) {
|
|
|
4633
5590
|
process.exit(1);
|
|
4634
5591
|
}
|
|
4635
5592
|
}
|
|
4636
|
-
async function
|
|
5593
|
+
async function run12() {
|
|
4637
5594
|
const verb = process.argv[2];
|
|
4638
5595
|
const jobId = process.argv[3];
|
|
4639
5596
|
try {
|
|
@@ -4652,31 +5609,31 @@ async function run11() {
|
|
|
4652
5609
|
process.exit(1);
|
|
4653
5610
|
}
|
|
4654
5611
|
}
|
|
4655
|
-
var __dirname4,
|
|
5612
|
+
var __dirname4, TERMINALHIRE_DIR9, INDEX_CACHE_FILE5;
|
|
4656
5613
|
var init_jpi_save = __esm({
|
|
4657
5614
|
"bin/jpi-save.js"() {
|
|
4658
5615
|
"use strict";
|
|
4659
5616
|
__dirname4 = fileURLToPath5(new URL(".", import.meta.url));
|
|
4660
|
-
|
|
4661
|
-
|
|
5617
|
+
TERMINALHIRE_DIR9 = join15(homedir13(), ".terminalhire");
|
|
5618
|
+
INDEX_CACHE_FILE5 = join15(TERMINALHIRE_DIR9, "index-cache.json");
|
|
4662
5619
|
}
|
|
4663
5620
|
});
|
|
4664
5621
|
|
|
4665
5622
|
// bin/jpi-dispatch.js
|
|
4666
5623
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
4667
|
-
import { join as
|
|
4668
|
-
import { existsSync as
|
|
5624
|
+
import { join as join16, dirname as dirname2 } from "path";
|
|
5625
|
+
import { existsSync as existsSync13, readFileSync as readFileSync15 } from "fs";
|
|
4669
5626
|
import { createRequire } from "module";
|
|
4670
5627
|
var __dirname5 = fileURLToPath6(new URL(".", import.meta.url));
|
|
4671
5628
|
function readPackageVersion() {
|
|
4672
5629
|
try {
|
|
4673
5630
|
const candidates = [
|
|
4674
|
-
|
|
4675
|
-
|
|
5631
|
+
join16(__dirname5, "..", "..", "package.json"),
|
|
5632
|
+
join16(__dirname5, "..", "package.json")
|
|
4676
5633
|
];
|
|
4677
5634
|
for (const p of candidates) {
|
|
4678
|
-
if (
|
|
4679
|
-
const pkg = JSON.parse(
|
|
5635
|
+
if (existsSync13(p)) {
|
|
5636
|
+
const pkg = JSON.parse(readFileSync15(p, "utf8"));
|
|
4680
5637
|
if (pkg.version) return pkg.version;
|
|
4681
5638
|
}
|
|
4682
5639
|
}
|
|
@@ -4687,7 +5644,7 @@ function readPackageVersion() {
|
|
|
4687
5644
|
var firstArg = process.argv[2];
|
|
4688
5645
|
if (!firstArg && !process.stdin.isTTY) {
|
|
4689
5646
|
const { default: childProcess } = await import("child_process");
|
|
4690
|
-
const nudgeScript =
|
|
5647
|
+
const nudgeScript = join16(__dirname5, "jpi.js");
|
|
4691
5648
|
const child = childProcess.spawnSync(process.execPath, [nudgeScript], {
|
|
4692
5649
|
stdio: ["inherit", "inherit", "inherit"]
|
|
4693
5650
|
});
|
|
@@ -4706,6 +5663,9 @@ if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-
|
|
|
4706
5663
|
console.log(" terminalhire jobs --remote-only Filter to remote roles only");
|
|
4707
5664
|
console.log(" terminalhire bounties Day-sized paid tasks you can knock out today");
|
|
4708
5665
|
console.log(" terminalhire bounties --priced Only bounties with a known $ amount");
|
|
5666
|
+
console.log(" terminalhire claim record <id|issueUrl> Claim a bounty locally + print the executor brief");
|
|
5667
|
+
console.log(" terminalhire claim list [--active] List your claims + accepted-PR rate");
|
|
5668
|
+
console.log(" terminalhire claim status [<id>] Poll source PR merge state (updates the metric)");
|
|
4709
5669
|
console.log(" terminalhire profile --show Display your encrypted local profile");
|
|
4710
5670
|
console.log(" terminalhire profile --edit Set displayName, contactEmail, prefs");
|
|
4711
5671
|
console.log(" terminalhire profile --delete Wipe profile and encryption key from disk");
|
|
@@ -4758,6 +5718,12 @@ if (firstArg === "bounties") {
|
|
|
4758
5718
|
await mod.run();
|
|
4759
5719
|
process.exit(0);
|
|
4760
5720
|
}
|
|
5721
|
+
if (firstArg === "claim") {
|
|
5722
|
+
process.argv.splice(2, 1);
|
|
5723
|
+
const mod = await Promise.resolve().then(() => (init_jpi_claim(), jpi_claim_exports));
|
|
5724
|
+
await mod.run();
|
|
5725
|
+
process.exit(0);
|
|
5726
|
+
}
|
|
4761
5727
|
if (firstArg === "profile") {
|
|
4762
5728
|
const mod = await Promise.resolve().then(() => (init_jpi_profile(), jpi_profile_exports));
|
|
4763
5729
|
await mod.run();
|