terminalhire 0.3.5 → 0.4.1
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 +963 -202
- package/dist/bin/jpi-dispatch.js +995 -209
- package/dist/bin/jpi-jobs.js +970 -204
- package/dist/bin/jpi-learn.js +98 -11
- package/dist/bin/jpi-login.js +986 -205
- package/dist/bin/jpi-profile.js +98 -11
- package/dist/bin/jpi-refresh.js +965 -204
- package/dist/bin/jpi-save.js +98 -11
- package/dist/bin/jpi-spinner.js +1 -1
- package/dist/bin/jpi-sync.js +98 -11
- package/dist/bin/spinner.js +2 -2
- package/dist/src/profile.js +40 -3
- package/dist/src/signal.js +40 -3
- package/package.json +1 -1
package/dist/bin/jpi-dispatch.js
CHANGED
|
@@ -361,11 +361,11 @@ var init_graph_data = __esm({
|
|
|
361
361
|
{ id: "spark", parents: ["data-engineering"], synonyms: ["apache-spark"] },
|
|
362
362
|
{ id: "airflow", parents: ["data-engineering"], synonyms: ["apache-airflow"] },
|
|
363
363
|
{ id: "dbt", parents: ["data-engineering"] },
|
|
364
|
-
{ id: "ml", synonyms: ["machine-learning"], related: [{ to: "pytorch", w: 0.5 }, { to: "tensorflow", w: 0.5 }, { to: "scikit-learn", w: 0.5 }] },
|
|
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 }] },
|
|
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", "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
|
-
{ id: "pandas", parents: ["python"], related: [{ to: "numpy", w: 0.6 }] },
|
|
368
|
+
{ id: "pandas", parents: ["python"], related: [{ to: "numpy", w: 0.6 }, { to: "data-engineering", w: 0.45 }, { to: "spark", w: 0.4 }] },
|
|
369
369
|
{ id: "numpy", parents: ["python"] },
|
|
370
370
|
{ id: "scikit-learn", parents: ["ml"], synonyms: ["sklearn"] },
|
|
371
371
|
{ id: "jupyter", parents: ["python"] },
|
|
@@ -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 }] },
|
|
@@ -540,6 +548,207 @@ var init_types2 = __esm({
|
|
|
540
548
|
}
|
|
541
549
|
});
|
|
542
550
|
|
|
551
|
+
// ../../packages/core/src/vocab/extract.ts
|
|
552
|
+
function tokenize(text) {
|
|
553
|
+
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
554
|
+
}
|
|
555
|
+
function looksLikeEngRole(title) {
|
|
556
|
+
return !NON_ENG_TITLE.test(title) && ENG_INTENT.test(title);
|
|
557
|
+
}
|
|
558
|
+
function resolveToken(token) {
|
|
559
|
+
const tryOne = (t) => {
|
|
560
|
+
if (GRAPH.ids.has(t)) return { id: t, viaSynonym: false };
|
|
561
|
+
const mapped = GRAPH.synonyms.get(t);
|
|
562
|
+
return mapped ? { id: mapped, viaSynonym: true } : null;
|
|
563
|
+
};
|
|
564
|
+
return tryOne(token) ?? tryOne(token.replace(/^[.\-+#]+|[.\-+#]+$/g, ""));
|
|
565
|
+
}
|
|
566
|
+
function extractSkillTags(title, body = "") {
|
|
567
|
+
if (!looksLikeEngRole(title)) return [];
|
|
568
|
+
const text = `${title}
|
|
569
|
+
${body}`;
|
|
570
|
+
const tokens = tokenize(text);
|
|
571
|
+
const ids = /* @__PURE__ */ new Set();
|
|
572
|
+
const ambiguousPending = /* @__PURE__ */ new Set();
|
|
573
|
+
for (const tok of tokens) {
|
|
574
|
+
const r = resolveToken(tok);
|
|
575
|
+
if (!r) continue;
|
|
576
|
+
if (NON_EXTRACTABLE.has(r.id)) continue;
|
|
577
|
+
if (SYNONYM_ONLY.has(r.id) && !r.viaSynonym) continue;
|
|
578
|
+
const cue = AMBIGUOUS[r.id];
|
|
579
|
+
if (cue) {
|
|
580
|
+
if (cue.test(text)) ids.add(r.id);
|
|
581
|
+
else ambiguousPending.add(r.id);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
ids.add(r.id);
|
|
585
|
+
}
|
|
586
|
+
const hardCount = [...ids].filter((id) => !SOFT_DOMAIN.has(id)).length;
|
|
587
|
+
if (hardCount >= 2) for (const id of ambiguousPending) ids.add(id);
|
|
588
|
+
return [...ids];
|
|
589
|
+
}
|
|
590
|
+
function coreTagsFromTitle(title) {
|
|
591
|
+
return extractSkillTags(title, "").filter((t) => !SOFT_DOMAIN.has(t));
|
|
592
|
+
}
|
|
593
|
+
var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, ENG_INTENT, NON_ENG_TITLE;
|
|
594
|
+
var init_extract = __esm({
|
|
595
|
+
"../../packages/core/src/vocab/extract.ts"() {
|
|
596
|
+
"use strict";
|
|
597
|
+
init_vocab();
|
|
598
|
+
SOFT_DOMAIN = /* @__PURE__ */ new Set([
|
|
599
|
+
"frontend",
|
|
600
|
+
"backend",
|
|
601
|
+
"devops",
|
|
602
|
+
"security",
|
|
603
|
+
"payments",
|
|
604
|
+
"billing",
|
|
605
|
+
"microservices",
|
|
606
|
+
"caching",
|
|
607
|
+
"search",
|
|
608
|
+
"observability",
|
|
609
|
+
"monitoring",
|
|
610
|
+
"testing",
|
|
611
|
+
"accessibility",
|
|
612
|
+
"seo",
|
|
613
|
+
"performance",
|
|
614
|
+
"realtime",
|
|
615
|
+
"authentication",
|
|
616
|
+
"api-design"
|
|
617
|
+
]);
|
|
618
|
+
SYNONYM_ONLY = /* @__PURE__ */ new Set(["performance", "security", "seo"]);
|
|
619
|
+
NON_EXTRACTABLE = /* @__PURE__ */ new Set(["payments", "billing"]);
|
|
620
|
+
for (const id of SYNONYM_ONLY) {
|
|
621
|
+
if (!SOFT_DOMAIN.has(id)) throw new Error(`extract: SYNONYM_ONLY "${id}" not in SOFT_DOMAIN`);
|
|
622
|
+
}
|
|
623
|
+
AMBIGUOUS = {
|
|
624
|
+
// Accept "go" with an ecosystem cue OR an explicit-skill phrasing ("Go developer",
|
|
625
|
+
// "in Go", "experience with Go"). Rejects prose: "ready to go", "go above", "go live".
|
|
626
|
+
go: /\b(golang|goroutines?|go\.mod|gin framework|gorm)\b|\bgo\b\s+(developer|engineer|programmer|microservices?|backend|services?|lang)|\b(in|with|using|written in|built in|experience (?:in|with)|proficient in|fluent in)\s+go\b/i,
|
|
627
|
+
r: /\b(rstudio|tidyverse|ggplot|shiny|dplyr|cran|r-lang|rlang)\b/i,
|
|
628
|
+
ml: /\b(machine[\s-]?learning|pytorch|tensorflow|scikit|sklearn|keras|neural|model training|deep[\s-]?learning|numpy|pandas|ml\s+(?:engineer|platform|researcher|infrastructure)|(?:ml|ai)\s+research)\b/i
|
|
629
|
+
};
|
|
630
|
+
ENG_INTENT = /\b(engineer|engineering|developer|dev\b|swe|sde|programmer|architect|full[\s-]?stack|front[\s-]?end|back[\s-]?end|devops|sre|software|coding|codebase|technical staff|tech(?:nical)? lead)\b/i;
|
|
631
|
+
NON_ENG_TITLE = /\b(account executive|account manager|sales (?:rep|representative|development|manager|lead)|sdr|bdr|recruiter|recruiting|talent|marketing|administrative|business partner|billing coordinator|operations (?:administrator|coordinator)|customer success|project finance|controller|bookkeeper|graphic|brand)\b/i;
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// ../../packages/core/src/vocab/idf-background.ts
|
|
636
|
+
var IDF_BACKGROUND;
|
|
637
|
+
var init_idf_background = __esm({
|
|
638
|
+
"../../packages/core/src/vocab/idf-background.ts"() {
|
|
639
|
+
"use strict";
|
|
640
|
+
IDF_BACKGROUND = {
|
|
641
|
+
N: 244,
|
|
642
|
+
df: {
|
|
643
|
+
"backend": 71,
|
|
644
|
+
"python": 57,
|
|
645
|
+
"monitoring": 44,
|
|
646
|
+
"nextjs": 40,
|
|
647
|
+
"testing": 40,
|
|
648
|
+
"observability": 38,
|
|
649
|
+
"llm": 38,
|
|
650
|
+
"go": 36,
|
|
651
|
+
"aws": 36,
|
|
652
|
+
"react": 33,
|
|
653
|
+
"frontend": 30,
|
|
654
|
+
"ml": 28,
|
|
655
|
+
"mobile": 24,
|
|
656
|
+
"realtime": 24,
|
|
657
|
+
"typescript": 23,
|
|
658
|
+
"devops": 22,
|
|
659
|
+
"kubernetes": 22,
|
|
660
|
+
"javascript": 21,
|
|
661
|
+
"java": 20,
|
|
662
|
+
"rag": 20,
|
|
663
|
+
"api-design": 20,
|
|
664
|
+
"linux": 19,
|
|
665
|
+
"postgresql": 19,
|
|
666
|
+
"search": 17,
|
|
667
|
+
"azure": 16,
|
|
668
|
+
"snowflake": 15,
|
|
669
|
+
"spark": 15,
|
|
670
|
+
"kotlin": 14,
|
|
671
|
+
"gcp": 14,
|
|
672
|
+
"accessibility": 14,
|
|
673
|
+
"nodejs": 14,
|
|
674
|
+
"graphql": 14,
|
|
675
|
+
"airflow": 14,
|
|
676
|
+
"docker": 14,
|
|
677
|
+
"ci-cd": 13,
|
|
678
|
+
"android": 12,
|
|
679
|
+
"cpp": 12,
|
|
680
|
+
"gitlab-ci": 11,
|
|
681
|
+
"anthropic": 11,
|
|
682
|
+
"terraform": 11,
|
|
683
|
+
"mysql": 11,
|
|
684
|
+
"r": 10,
|
|
685
|
+
"dbt": 9,
|
|
686
|
+
"langchain": 9,
|
|
687
|
+
"pytorch": 9,
|
|
688
|
+
"ruby": 9,
|
|
689
|
+
"rails": 9,
|
|
690
|
+
"cloudflare": 7,
|
|
691
|
+
"datadog": 7,
|
|
692
|
+
"css": 7,
|
|
693
|
+
"ansible": 7,
|
|
694
|
+
"openai": 6,
|
|
695
|
+
"kafka": 6,
|
|
696
|
+
"rust": 5,
|
|
697
|
+
"grpc": 5,
|
|
698
|
+
"microservices": 5,
|
|
699
|
+
"serverless": 5,
|
|
700
|
+
"scala": 5,
|
|
701
|
+
"prometheus": 5,
|
|
702
|
+
"grafana": 5,
|
|
703
|
+
"php": 5,
|
|
704
|
+
"redis": 5,
|
|
705
|
+
"huggingface": 4,
|
|
706
|
+
"pandas": 4,
|
|
707
|
+
"scikit-learn": 4,
|
|
708
|
+
"html": 4,
|
|
709
|
+
"ios": 4,
|
|
710
|
+
"authentication": 4,
|
|
711
|
+
"vue": 4,
|
|
712
|
+
"mlops": 3,
|
|
713
|
+
"spring": 3,
|
|
714
|
+
"mongodb": 3,
|
|
715
|
+
"csharp": 3,
|
|
716
|
+
"swift": 2,
|
|
717
|
+
"caching": 2,
|
|
718
|
+
"haskell": 2,
|
|
719
|
+
"pulumi": 2,
|
|
720
|
+
"argocd": 2,
|
|
721
|
+
"tensorflow": 2,
|
|
722
|
+
"express": 2,
|
|
723
|
+
"elasticsearch": 2,
|
|
724
|
+
"clickhouse": 2,
|
|
725
|
+
"nestjs": 2,
|
|
726
|
+
"vite": 2,
|
|
727
|
+
"svelte": 2,
|
|
728
|
+
"phoenix": 2,
|
|
729
|
+
"angular": 2,
|
|
730
|
+
"django": 2,
|
|
731
|
+
"dotnet": 2,
|
|
732
|
+
"elixir": 2,
|
|
733
|
+
"bun": 1,
|
|
734
|
+
"oauth": 1,
|
|
735
|
+
"dynamodb": 1,
|
|
736
|
+
"helm": 1,
|
|
737
|
+
"playwright": 1,
|
|
738
|
+
"cypress": 1,
|
|
739
|
+
"jest": 1,
|
|
740
|
+
"mocha": 1,
|
|
741
|
+
"typeorm": 1,
|
|
742
|
+
"tailwind": 1,
|
|
743
|
+
"prisma": 1,
|
|
744
|
+
"expo": 1,
|
|
745
|
+
"rabbitmq": 1,
|
|
746
|
+
"redux": 1
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
|
|
543
752
|
// ../../packages/core/src/vocab/index.ts
|
|
544
753
|
function normalize(tokens) {
|
|
545
754
|
const result = /* @__PURE__ */ new Set();
|
|
@@ -576,6 +785,8 @@ var init_vocab = __esm({
|
|
|
576
785
|
init_types2();
|
|
577
786
|
init_closure();
|
|
578
787
|
init_graph_data();
|
|
788
|
+
init_extract();
|
|
789
|
+
init_idf_background();
|
|
579
790
|
GRAPH = buildGraph(VOCAB_NODES);
|
|
580
791
|
VOCABULARY = [...GRAPH.ids];
|
|
581
792
|
SYNONYMS = Object.fromEntries(GRAPH.synonyms);
|
|
@@ -590,23 +801,330 @@ var init_vocabulary = __esm({
|
|
|
590
801
|
}
|
|
591
802
|
});
|
|
592
803
|
|
|
593
|
-
// ../../packages/core/src/
|
|
594
|
-
function
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
804
|
+
// ../../packages/core/src/github.ts
|
|
805
|
+
function ghHeaders(token) {
|
|
806
|
+
const headers = {
|
|
807
|
+
Accept: "application/vnd.github+json",
|
|
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"
|
|
812
|
+
};
|
|
813
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
814
|
+
return headers;
|
|
815
|
+
}
|
|
816
|
+
async function ghFetch(path, token) {
|
|
817
|
+
const url = `https://api.github.com${path}`;
|
|
818
|
+
const res = await fetch(url, { headers: ghHeaders(token) });
|
|
819
|
+
if (!res.ok) {
|
|
820
|
+
throw new Error(`GitHub API ${path}: HTTP ${res.status} ${res.statusText}`);
|
|
821
|
+
}
|
|
822
|
+
return res.json();
|
|
823
|
+
}
|
|
824
|
+
async function fetchGitHubProfile(login, token) {
|
|
825
|
+
const user = await ghFetch(`/users/${login}`, token);
|
|
826
|
+
let repos = [];
|
|
827
|
+
try {
|
|
828
|
+
repos = await ghFetch(
|
|
829
|
+
`/users/${login}/repos?sort=pushed&per_page=100`,
|
|
830
|
+
token
|
|
831
|
+
);
|
|
832
|
+
} catch (err) {
|
|
833
|
+
console.warn(`[github] ${login}: repos fetch failed, continuing \u2014`, err);
|
|
834
|
+
}
|
|
835
|
+
const langCount = {};
|
|
836
|
+
for (const repo of repos) {
|
|
837
|
+
if (repo.fork) continue;
|
|
838
|
+
if (repo.language) {
|
|
839
|
+
langCount[repo.language.toLowerCase()] = (langCount[repo.language.toLowerCase()] ?? 0) + 1;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
const topLanguages = Object.entries(langCount).sort(([, a], [, b]) => b - a).slice(0, 10).map(([lang]) => lang);
|
|
843
|
+
const topicSet = /* @__PURE__ */ new Set();
|
|
844
|
+
for (const repo of repos) {
|
|
845
|
+
if (repo.fork) continue;
|
|
846
|
+
for (const t of repo.topics ?? []) topicSet.add(t.toLowerCase());
|
|
847
|
+
}
|
|
848
|
+
const topics = Array.from(topicSet).slice(0, 30);
|
|
849
|
+
let recentPRorgs;
|
|
850
|
+
try {
|
|
851
|
+
const q = encodeURIComponent(
|
|
852
|
+
`type:pr is:merged author:${login} sort:updated`
|
|
853
|
+
);
|
|
854
|
+
const result = await ghFetch(
|
|
855
|
+
`/search/issues?q=${q}&per_page=30`,
|
|
856
|
+
token
|
|
857
|
+
);
|
|
858
|
+
const orgs = /* @__PURE__ */ new Set();
|
|
859
|
+
for (const item of result.items ?? []) {
|
|
860
|
+
const orgLogin = item.repository?.owner?.login;
|
|
861
|
+
if (orgLogin && orgLogin !== login) orgs.add(orgLogin);
|
|
862
|
+
}
|
|
863
|
+
if (orgs.size > 0) recentPRorgs = Array.from(orgs);
|
|
864
|
+
} catch {
|
|
865
|
+
}
|
|
866
|
+
return {
|
|
867
|
+
login: user.login,
|
|
868
|
+
name: user.name ?? void 0,
|
|
869
|
+
publicEmail: user.email ?? void 0,
|
|
870
|
+
avatarUrl: user.avatar_url,
|
|
871
|
+
accountCreatedAt: user.created_at,
|
|
872
|
+
publicRepos: user.public_repos,
|
|
873
|
+
followers: user.followers,
|
|
874
|
+
topLanguages,
|
|
875
|
+
topics,
|
|
876
|
+
recentPRorgs
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
function inferSeniority(p) {
|
|
880
|
+
const ageMs = Date.now() - new Date(p.accountCreatedAt).getTime();
|
|
881
|
+
const ageYears = ageMs / (1e3 * 60 * 60 * 24 * 365.25);
|
|
882
|
+
if (ageYears >= 9 && (p.publicRepos >= 40 || p.followers >= 500)) return "staff";
|
|
883
|
+
if (ageYears >= 5 && (p.publicRepos >= 20 || p.followers >= 100)) return "senior";
|
|
884
|
+
if (ageYears >= 2 && p.publicRepos >= 5) return "mid";
|
|
885
|
+
return "junior";
|
|
886
|
+
}
|
|
887
|
+
function githubToFingerprint(p) {
|
|
888
|
+
const rawTokens = [
|
|
889
|
+
...p.topLanguages,
|
|
890
|
+
...p.topics
|
|
891
|
+
// recentPRorgs intentionally excluded — org names are not skill tags
|
|
892
|
+
];
|
|
893
|
+
const skillTags = normalize(rawTokens);
|
|
894
|
+
const seniorityBand = inferSeniority(p);
|
|
895
|
+
return { skillTags, seniorityBand };
|
|
896
|
+
}
|
|
897
|
+
async function ghFetchRaw(path, token) {
|
|
898
|
+
return fetch(`https://api.github.com${path}`, { headers: ghHeaders(token) });
|
|
899
|
+
}
|
|
900
|
+
function parseRepoUrl(repoUrl) {
|
|
901
|
+
const m = repoUrl.match(/\/repos\/([^/]+)\/([^/]+)\/?$/);
|
|
902
|
+
return m ? { owner: m[1], name: m[2] } : null;
|
|
903
|
+
}
|
|
904
|
+
function isTrivialPRTitle(title) {
|
|
905
|
+
return TRIVIAL_PR_TITLE.test(title);
|
|
906
|
+
}
|
|
907
|
+
async function fetchOwnedOrgs(token) {
|
|
908
|
+
try {
|
|
909
|
+
const memberships = await ghFetch(`/user/memberships/orgs?per_page=100`, token);
|
|
910
|
+
return new Set(
|
|
911
|
+
memberships.filter((m) => m.role === "admin").map((m) => m.organization.login.toLowerCase())
|
|
912
|
+
);
|
|
913
|
+
} catch {
|
|
914
|
+
return /* @__PURE__ */ new Set();
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
async function repoContributorCount(owner, name, token) {
|
|
918
|
+
try {
|
|
919
|
+
const res = await ghFetchRaw(
|
|
920
|
+
`/repos/${owner}/${name}/contributors?per_page=1&anon=false`,
|
|
921
|
+
token
|
|
922
|
+
);
|
|
923
|
+
if (!res.ok) return void 0;
|
|
924
|
+
const link = res.headers.get("link");
|
|
925
|
+
const m = link?.match(/[?&]page=(\d+)>;\s*rel="last"/);
|
|
926
|
+
if (m) return Number(m[1]);
|
|
927
|
+
const body = await res.json();
|
|
928
|
+
return Array.isArray(body) ? body.length : 0;
|
|
929
|
+
} catch {
|
|
930
|
+
return void 0;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
async function fetchRepoMeta(owner, name, token, cache) {
|
|
934
|
+
const key = `${owner}/${name}`.toLowerCase();
|
|
935
|
+
const cached = cache.get(key);
|
|
936
|
+
if (cached !== void 0) return cached;
|
|
937
|
+
let meta = null;
|
|
938
|
+
try {
|
|
939
|
+
const r = await ghFetch(`/repos/${owner}/${name}`, token);
|
|
940
|
+
const contributors = await repoContributorCount(owner, name, token);
|
|
941
|
+
meta = {
|
|
942
|
+
stars: r.stargazers_count ?? 0,
|
|
943
|
+
archived: !!r.archived,
|
|
944
|
+
fork: !!r.fork,
|
|
945
|
+
language: r.language ?? null,
|
|
946
|
+
topics: r.topics ?? [],
|
|
947
|
+
contributors
|
|
948
|
+
};
|
|
949
|
+
} catch {
|
|
950
|
+
meta = null;
|
|
951
|
+
}
|
|
952
|
+
cache.set(key, meta);
|
|
953
|
+
return meta;
|
|
954
|
+
}
|
|
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
|
+
}) {
|
|
973
|
+
const computedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
974
|
+
const loginLc = login.toLowerCase();
|
|
975
|
+
let items;
|
|
976
|
+
try {
|
|
977
|
+
const q = encodeURIComponent(`type:pr is:merged author:${login} -user:${login} sort:updated`);
|
|
978
|
+
const res = await ghFetch(
|
|
979
|
+
`/search/issues?q=${q}&per_page=${CANDIDATE_PR_PAGE}`,
|
|
980
|
+
token
|
|
981
|
+
);
|
|
982
|
+
items = res.items ?? [];
|
|
983
|
+
} catch (err) {
|
|
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");
|
|
987
|
+
}
|
|
988
|
+
const byDomain = {};
|
|
989
|
+
let qualifyingTotal = 0;
|
|
990
|
+
for (const item of items) {
|
|
991
|
+
const repo = parseRepoUrl(item.repository_url);
|
|
992
|
+
if (!repo) continue;
|
|
993
|
+
const ownerLc = repo.owner.toLowerCase();
|
|
994
|
+
if (ownerLc === loginLc) continue;
|
|
995
|
+
if (ownedOrgs.has(ownerLc)) continue;
|
|
996
|
+
if (isTrivialPRTitle(item.title)) continue;
|
|
997
|
+
const meta = await fetchRepoMeta(repo.owner, repo.name, token, cache);
|
|
998
|
+
if (!meta) continue;
|
|
999
|
+
if (meta.archived || meta.fork) continue;
|
|
1000
|
+
if (meta.stars < gates.minStars) continue;
|
|
1001
|
+
if (meta.contributors !== void 0 && meta.contributors < gates.minContributors) continue;
|
|
1002
|
+
qualifyingTotal += 1;
|
|
1003
|
+
const mergedAt = item.pull_request?.merged_at ?? item.closed_at ?? item.created_at;
|
|
1004
|
+
const rawDomains = [meta.language ?? "", ...meta.topics].filter(Boolean);
|
|
1005
|
+
for (const d of new Set(normalize(rawDomains))) {
|
|
1006
|
+
const b = byDomain[d] ?? (byDomain[d] = { mergedPRs: 0, distinctOrgs: 0, lastMergedAt: mergedAt, orgs: /* @__PURE__ */ new Set() });
|
|
1007
|
+
b.mergedPRs += 1;
|
|
1008
|
+
b.orgs.add(ownerLc);
|
|
1009
|
+
if (mergedAt > b.lastMergedAt) b.lastMergedAt = mergedAt;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
const finalDomains = {};
|
|
1013
|
+
for (const [d, b] of Object.entries(byDomain)) {
|
|
1014
|
+
finalDomains[d] = {
|
|
1015
|
+
mergedPRs: b.mergedPRs,
|
|
1016
|
+
distinctOrgs: b.orgs.size,
|
|
1017
|
+
lastMergedAt: b.lastMergedAt
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
return { status: "ok", byDomain: finalDomains, qualifyingTotal, computedAt };
|
|
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
|
+
}
|
|
1034
|
+
function acceptanceCountForDomains(cred, domains) {
|
|
1035
|
+
if (cred.status !== "ok") return 0;
|
|
1036
|
+
let max = 0;
|
|
1037
|
+
for (const d of domains) {
|
|
1038
|
+
const c = cred.byDomain[d]?.mergedPRs ?? 0;
|
|
1039
|
+
if (c > max) max = c;
|
|
1040
|
+
}
|
|
1041
|
+
return max;
|
|
1042
|
+
}
|
|
1043
|
+
function bestAcceptanceDomain(cred, domains) {
|
|
1044
|
+
if (cred.status !== "ok") return null;
|
|
1045
|
+
let best = null;
|
|
1046
|
+
for (const d of domains) {
|
|
1047
|
+
const count = cred.byDomain[d]?.mergedPRs ?? 0;
|
|
1048
|
+
if (count > 0 && (best === null || count > best.count)) best = { domain: d, count };
|
|
1049
|
+
}
|
|
1050
|
+
return best;
|
|
1051
|
+
}
|
|
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);
|
|
601
1086
|
}
|
|
602
1087
|
}
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
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
|
+
});
|
|
606
1102
|
}
|
|
607
|
-
return
|
|
1103
|
+
return scored.sort((a, b) => b.weight - a.weight).slice(0, 12).map((s) => s.t);
|
|
608
1104
|
}
|
|
609
|
-
|
|
1105
|
+
var MIN_STARS, MIN_CONTRIBUTORS, CANDIDATE_PR_PAGE, TRIVIAL_PR_TITLE, RESUME_DECAY_HALF_LIFE_MS, RESUME_MIN_SCORE;
|
|
1106
|
+
var init_github = __esm({
|
|
1107
|
+
"../../packages/core/src/github.ts"() {
|
|
1108
|
+
"use strict";
|
|
1109
|
+
init_vocabulary();
|
|
1110
|
+
MIN_STARS = 50;
|
|
1111
|
+
MIN_CONTRIBUTORS = 10;
|
|
1112
|
+
CANDIDATE_PR_PAGE = 50;
|
|
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;
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
// ../../packages/core/src/matcher.ts
|
|
1120
|
+
function acceptanceDomainsOf(job) {
|
|
1121
|
+
return job.coreTags && job.coreTags.length > 0 ? job.coreTags : job.tags;
|
|
1122
|
+
}
|
|
1123
|
+
function backgroundIdf(tag) {
|
|
1124
|
+
const df = IDF_BACKGROUND.df[tag] ?? 0;
|
|
1125
|
+
return Math.log((IDF_BACKGROUND.N + 1) / (df + 1)) + 1;
|
|
1126
|
+
}
|
|
1127
|
+
function inferSeniority2(title) {
|
|
610
1128
|
if (!ENG_TITLE.test(title)) return void 0;
|
|
611
1129
|
for (const [re, level] of SENIORITY_PATTERNS) {
|
|
612
1130
|
if (re.test(title)) return level;
|
|
@@ -615,7 +1133,7 @@ function inferSeniority(title) {
|
|
|
615
1133
|
}
|
|
616
1134
|
function seniorityScore(fp, job) {
|
|
617
1135
|
if (!fp.seniorityBand) return 1;
|
|
618
|
-
const jobLevel =
|
|
1136
|
+
const jobLevel = inferSeniority2(job.title);
|
|
619
1137
|
if (!jobLevel) return 0.85;
|
|
620
1138
|
const wanted = SENIORITY_RANK[fp.seniorityBand] ?? 1;
|
|
621
1139
|
const got = SENIORITY_RANK[jobLevel] ?? 1;
|
|
@@ -625,8 +1143,10 @@ function seniorityScore(fp, job) {
|
|
|
625
1143
|
return 0.4;
|
|
626
1144
|
}
|
|
627
1145
|
function recencyScore(postedAt, now) {
|
|
628
|
-
if (!postedAt) return
|
|
629
|
-
const
|
|
1146
|
+
if (!postedAt) return UNKNOWN_RECENCY;
|
|
1147
|
+
const ms = new Date(postedAt).getTime();
|
|
1148
|
+
if (Number.isNaN(ms)) return UNKNOWN_RECENCY;
|
|
1149
|
+
const ageDays2 = (now - ms) / 864e5;
|
|
630
1150
|
if (ageDays2 < 7) return 1;
|
|
631
1151
|
if (ageDays2 < 30) return 0.9;
|
|
632
1152
|
if (ageDays2 < 90) return 0.75;
|
|
@@ -657,9 +1177,8 @@ function harmonicMean(a, b) {
|
|
|
657
1177
|
if (a <= 0 || b <= 0) return 0;
|
|
658
1178
|
return 2 * a * b / (a + b);
|
|
659
1179
|
}
|
|
660
|
-
function match(fp, jobs, limit = 5, now = Date.now()) {
|
|
661
|
-
const
|
|
662
|
-
const idfOf = (t) => idf.get(t) ?? 0;
|
|
1180
|
+
function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
|
|
1181
|
+
const idfOf = backgroundIdf;
|
|
663
1182
|
const expanded = expandWeighted(fp.skillTags);
|
|
664
1183
|
const maxDevScore = fp.skillTags.reduce((acc, t) => acc + idfOf(t), 0);
|
|
665
1184
|
const candidates = jobs.filter((j) => passesFilters(fp, j));
|
|
@@ -685,32 +1204,45 @@ function match(fp, jobs, limit = 5, now = Date.now()) {
|
|
|
685
1204
|
const jobCov = jobMaxScore > 0 ? Math.min(1, jobMatchScore / jobMaxScore) : 0;
|
|
686
1205
|
const tagComponent = harmonicMean(devCov, jobCov);
|
|
687
1206
|
if (tagComponent === 0) return null;
|
|
1207
|
+
const coreTags = job.coreTags ?? coreTagsFromTitle(job.title);
|
|
1208
|
+
let coreComponent = tagComponent;
|
|
1209
|
+
if (coreTags.length > 0) {
|
|
1210
|
+
const coreCov = Math.max(0, ...coreTags.map((ct) => expanded.get(ct)?.weight ?? 0));
|
|
1211
|
+
if (coreCov === 0) coreComponent = tagComponent * CORE_MISS_PENALTY;
|
|
1212
|
+
}
|
|
688
1213
|
details.sort((a, b) => idfOf(b.tag) * b.weight - idfOf(a.tag) * a.weight);
|
|
689
1214
|
const sScore = seniorityScore(fp, job);
|
|
690
1215
|
const rScore = recencyScore(job.postedAt, now);
|
|
691
|
-
const score =
|
|
1216
|
+
const score = coreComponent * 0.6 + sScore * 0.25 + rScore * 0.15;
|
|
692
1217
|
const matchedTags = [...new Set(details.map((d) => d.via ?? d.tag))];
|
|
1218
|
+
const badge = opts.acceptance ? bestAcceptanceDomain(opts.acceptance, acceptanceDomainsOf(job)) : null;
|
|
693
1219
|
return {
|
|
694
1220
|
job,
|
|
695
1221
|
score: Math.round(score * 1e3) / 1e3,
|
|
696
1222
|
matchedTags,
|
|
697
1223
|
matchDetails: details,
|
|
1224
|
+
...badge ? { acceptance: { status: "ok", domain: badge.domain, count: badge.count } } : {},
|
|
698
1225
|
reason: buildReason(details)
|
|
699
1226
|
};
|
|
700
1227
|
});
|
|
701
|
-
return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) =>
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1228
|
+
return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => {
|
|
1229
|
+
const byScore = b.score - a.score;
|
|
1230
|
+
if (Math.abs(byScore) > TIEBREAK_EPS) return byScore;
|
|
1231
|
+
const byAcceptance = (b.acceptance?.count ?? 0) - (a.acceptance?.count ?? 0);
|
|
1232
|
+
if (byAcceptance !== 0) return byAcceptance;
|
|
1233
|
+
return byScore;
|
|
1234
|
+
}).slice(0, limit);
|
|
1235
|
+
}
|
|
1236
|
+
var MIN_SCORE, TIEBREAK_EPS, SHARPEN, CORE_MISS_PENALTY, SENIORITY_RANK, SENIORITY_PATTERNS, ENG_TITLE, UNKNOWN_RECENCY;
|
|
708
1237
|
var init_matcher = __esm({
|
|
709
1238
|
"../../packages/core/src/matcher.ts"() {
|
|
710
1239
|
"use strict";
|
|
711
1240
|
init_vocabulary();
|
|
1241
|
+
init_github();
|
|
712
1242
|
MIN_SCORE = 0.15;
|
|
1243
|
+
TIEBREAK_EPS = 5e-3;
|
|
713
1244
|
SHARPEN = 1.6;
|
|
1245
|
+
CORE_MISS_PENALTY = 0.4;
|
|
714
1246
|
SENIORITY_RANK = {
|
|
715
1247
|
junior: 0,
|
|
716
1248
|
mid: 1,
|
|
@@ -724,24 +1256,31 @@ var init_matcher = __esm({
|
|
|
724
1256
|
[/\bmid[\s-]?level\b|\bmid\b/i, "mid"]
|
|
725
1257
|
];
|
|
726
1258
|
ENG_TITLE = /\b(engineer|engineering|developer|dev|swe|sde|programmer|architect)\b/i;
|
|
1259
|
+
UNKNOWN_RECENCY = 0.75;
|
|
727
1260
|
}
|
|
728
1261
|
});
|
|
729
1262
|
|
|
730
|
-
// ../../packages/core/src/feeds/
|
|
731
|
-
function
|
|
732
|
-
return
|
|
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) });
|
|
733
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
|
+
|
|
1275
|
+
// ../../packages/core/src/feeds/greenhouse.ts
|
|
734
1276
|
function extractTags(job) {
|
|
735
|
-
const
|
|
736
|
-
job.title,
|
|
1277
|
+
const body = [
|
|
737
1278
|
...(job.departments ?? []).map((d) => d.name),
|
|
738
1279
|
job.location?.name ?? "",
|
|
739
1280
|
...(job.offices ?? []).map((o) => o.name),
|
|
740
|
-
// mine the full HTML description for additional signal when present
|
|
741
1281
|
...job.content ? [job.content.replace(/<[^>]*>/g, " ")] : []
|
|
742
|
-
].filter(Boolean);
|
|
743
|
-
|
|
744
|
-
return normalize(tokens);
|
|
1282
|
+
].filter(Boolean).join(" ");
|
|
1283
|
+
return extractSkillTags(job.title, body);
|
|
745
1284
|
}
|
|
746
1285
|
function inferRemote(location) {
|
|
747
1286
|
const l = location.toLowerCase();
|
|
@@ -751,7 +1290,7 @@ async function fetchSlug(slug) {
|
|
|
751
1290
|
const url = `https://boards-api.greenhouse.io/v1/boards/${slug}/jobs?content=true`;
|
|
752
1291
|
let res;
|
|
753
1292
|
try {
|
|
754
|
-
res = await
|
|
1293
|
+
res = await fetchWithTimeout(url, { headers: { Accept: "application/json" } });
|
|
755
1294
|
} catch (err) {
|
|
756
1295
|
console.warn(`[greenhouse] ${slug}: network error \u2014`, err);
|
|
757
1296
|
return [];
|
|
@@ -793,6 +1332,7 @@ var init_greenhouse = __esm({
|
|
|
793
1332
|
"../../packages/core/src/feeds/greenhouse.ts"() {
|
|
794
1333
|
"use strict";
|
|
795
1334
|
init_vocabulary();
|
|
1335
|
+
init_http();
|
|
796
1336
|
FALLBACK_SLUGS = [
|
|
797
1337
|
"stripe",
|
|
798
1338
|
"linear",
|
|
@@ -839,17 +1379,15 @@ var init_greenhouse = __esm({
|
|
|
839
1379
|
});
|
|
840
1380
|
|
|
841
1381
|
// ../../packages/core/src/feeds/ashby.ts
|
|
842
|
-
function tokenize2(text) {
|
|
843
|
-
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
844
|
-
}
|
|
845
1382
|
function extractTags2(job) {
|
|
846
|
-
const
|
|
847
|
-
job.
|
|
848
|
-
job.
|
|
849
|
-
job.
|
|
850
|
-
...(job.secondaryLocations ?? []).map((l) => l.
|
|
851
|
-
|
|
852
|
-
|
|
1383
|
+
const body = [
|
|
1384
|
+
job.team ?? "",
|
|
1385
|
+
job.department ?? "",
|
|
1386
|
+
job.location ?? "",
|
|
1387
|
+
...(job.secondaryLocations ?? []).map((l) => l.location ?? ""),
|
|
1388
|
+
job.descriptionPlain ?? ""
|
|
1389
|
+
].join(" ");
|
|
1390
|
+
return extractSkillTags(job.title, body);
|
|
853
1391
|
}
|
|
854
1392
|
function mapEmploymentType(raw) {
|
|
855
1393
|
if (!raw) return "full_time";
|
|
@@ -860,12 +1398,12 @@ function mapEmploymentType(raw) {
|
|
|
860
1398
|
}
|
|
861
1399
|
function inferRemote2(job) {
|
|
862
1400
|
if (job.isRemote === true) return true;
|
|
863
|
-
const loc = (job.
|
|
1401
|
+
const loc = (job.location ?? "").toLowerCase();
|
|
864
1402
|
return loc.includes("remote") || loc.includes("anywhere");
|
|
865
1403
|
}
|
|
866
1404
|
async function fetchSlug2(slug) {
|
|
867
1405
|
const url = `https://api.ashbyhq.com/posting-api/job-board/${slug}`;
|
|
868
|
-
const res = await
|
|
1406
|
+
const res = await fetchWithTimeout(url, {
|
|
869
1407
|
headers: { Accept: "application/json" }
|
|
870
1408
|
});
|
|
871
1409
|
if (!res.ok) {
|
|
@@ -879,14 +1417,14 @@ async function fetchSlug2(slug) {
|
|
|
879
1417
|
source: "ashby",
|
|
880
1418
|
title: j.title,
|
|
881
1419
|
company: slug,
|
|
882
|
-
url: j.applyUrl ?? `https://jobs.ashbyhq.com/${slug}/${j.id}`,
|
|
1420
|
+
url: j.jobUrl ?? j.applyUrl ?? `https://jobs.ashbyhq.com/${slug}/${j.id}`,
|
|
883
1421
|
remote: inferRemote2(j),
|
|
884
|
-
location: j.
|
|
1422
|
+
location: j.location,
|
|
885
1423
|
compMin: comp?.minValue,
|
|
886
1424
|
compMax: comp?.maxValue,
|
|
887
1425
|
tags: extractTags2(j),
|
|
888
1426
|
roleType: mapEmploymentType(j.employmentType),
|
|
889
|
-
postedAt: j.
|
|
1427
|
+
postedAt: j.publishedAt,
|
|
890
1428
|
applyMode: "direct",
|
|
891
1429
|
raw: j
|
|
892
1430
|
};
|
|
@@ -897,6 +1435,7 @@ var init_ashby = __esm({
|
|
|
897
1435
|
"../../packages/core/src/feeds/ashby.ts"() {
|
|
898
1436
|
"use strict";
|
|
899
1437
|
init_vocabulary();
|
|
1438
|
+
init_http();
|
|
900
1439
|
ashby = {
|
|
901
1440
|
source: "ashby",
|
|
902
1441
|
async fetch(opts) {
|
|
@@ -913,20 +1452,16 @@ var init_ashby = __esm({
|
|
|
913
1452
|
});
|
|
914
1453
|
|
|
915
1454
|
// ../../packages/core/src/feeds/lever.ts
|
|
916
|
-
function tokenize3(text) {
|
|
917
|
-
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
918
|
-
}
|
|
919
1455
|
function extractTags3(p) {
|
|
920
1456
|
const cat = p.categories ?? {};
|
|
921
|
-
const
|
|
922
|
-
p.text,
|
|
1457
|
+
const body = [
|
|
923
1458
|
cat.team ?? "",
|
|
924
1459
|
cat.department ?? "",
|
|
925
1460
|
cat.location ?? "",
|
|
926
1461
|
...cat.allLocations ?? [],
|
|
927
1462
|
p.descriptionPlain ?? ""
|
|
928
|
-
];
|
|
929
|
-
return
|
|
1463
|
+
].join(" ");
|
|
1464
|
+
return extractSkillTags(p.text, body);
|
|
930
1465
|
}
|
|
931
1466
|
function mapCommitment(raw) {
|
|
932
1467
|
if (!raw) return "full_time";
|
|
@@ -951,7 +1486,7 @@ function toIso(ms) {
|
|
|
951
1486
|
}
|
|
952
1487
|
async function fetchSlug3(slug) {
|
|
953
1488
|
const url = `https://api.lever.co/v0/postings/${slug}?mode=json`;
|
|
954
|
-
const res = await
|
|
1489
|
+
const res = await fetchWithTimeout(url, { headers: { Accept: "application/json" } });
|
|
955
1490
|
if (!res.ok) {
|
|
956
1491
|
throw new Error(`Lever ${slug}: HTTP ${res.status}`);
|
|
957
1492
|
}
|
|
@@ -982,6 +1517,7 @@ var init_lever = __esm({
|
|
|
982
1517
|
"../../packages/core/src/feeds/lever.ts"() {
|
|
983
1518
|
"use strict";
|
|
984
1519
|
init_vocabulary();
|
|
1520
|
+
init_http();
|
|
985
1521
|
lever = {
|
|
986
1522
|
source: "lever",
|
|
987
1523
|
async fetch(opts) {
|
|
@@ -999,15 +1535,8 @@ var init_lever = __esm({
|
|
|
999
1535
|
});
|
|
1000
1536
|
|
|
1001
1537
|
// ../../packages/core/src/feeds/himalayas.ts
|
|
1002
|
-
function tokenize4(text) {
|
|
1003
|
-
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
1004
|
-
}
|
|
1005
1538
|
function extractTags4(job) {
|
|
1006
|
-
|
|
1007
|
-
job.title,
|
|
1008
|
-
...job.tags ?? []
|
|
1009
|
-
];
|
|
1010
|
-
return normalize(texts.flatMap(tokenize4));
|
|
1539
|
+
return extractSkillTags(job.title, (job.tags ?? []).join(" "));
|
|
1011
1540
|
}
|
|
1012
1541
|
function mapJobType(raw) {
|
|
1013
1542
|
if (!raw) return "full_time";
|
|
@@ -1030,12 +1559,13 @@ var init_himalayas = __esm({
|
|
|
1030
1559
|
"../../packages/core/src/feeds/himalayas.ts"() {
|
|
1031
1560
|
"use strict";
|
|
1032
1561
|
init_vocabulary();
|
|
1562
|
+
init_http();
|
|
1033
1563
|
himalayas = {
|
|
1034
1564
|
source: "himalayas",
|
|
1035
1565
|
async fetch(opts) {
|
|
1036
1566
|
const limit = opts?.limit ?? 100;
|
|
1037
1567
|
const url = `https://himalayas.app/jobs/api?limit=${limit}`;
|
|
1038
|
-
const res = await
|
|
1568
|
+
const res = await fetchWithTimeout(url, {
|
|
1039
1569
|
headers: { Accept: "application/json" }
|
|
1040
1570
|
});
|
|
1041
1571
|
if (!res.ok) {
|
|
@@ -1092,9 +1622,6 @@ var init_entities = __esm({
|
|
|
1092
1622
|
});
|
|
1093
1623
|
|
|
1094
1624
|
// ../../packages/core/src/feeds/wwr.ts
|
|
1095
|
-
function tokenize5(text) {
|
|
1096
|
-
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
1097
|
-
}
|
|
1098
1625
|
function stripHtml(html) {
|
|
1099
1626
|
return html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
1100
1627
|
}
|
|
@@ -1138,8 +1665,8 @@ function parseRss(xml) {
|
|
|
1138
1665
|
return items;
|
|
1139
1666
|
}
|
|
1140
1667
|
function extractTags5(item) {
|
|
1141
|
-
const
|
|
1142
|
-
return
|
|
1668
|
+
const body = [item.category, stripHtml(item.description)].join(" ");
|
|
1669
|
+
return extractSkillTags(item.title, body);
|
|
1143
1670
|
}
|
|
1144
1671
|
var WWR_RSS_URL, wwr;
|
|
1145
1672
|
var init_wwr = __esm({
|
|
@@ -1147,12 +1674,13 @@ var init_wwr = __esm({
|
|
|
1147
1674
|
"use strict";
|
|
1148
1675
|
init_vocabulary();
|
|
1149
1676
|
init_entities();
|
|
1677
|
+
init_http();
|
|
1150
1678
|
WWR_RSS_URL = "https://weworkremotely.com/remote-jobs.rss";
|
|
1151
1679
|
wwr = {
|
|
1152
1680
|
source: "wwr",
|
|
1153
1681
|
async fetch(opts) {
|
|
1154
1682
|
const limit = opts?.limit ?? 200;
|
|
1155
|
-
const res = await
|
|
1683
|
+
const res = await fetchWithTimeout(WWR_RSS_URL, {
|
|
1156
1684
|
headers: { Accept: "application/rss+xml, application/xml, text/xml" }
|
|
1157
1685
|
});
|
|
1158
1686
|
if (!res.ok) {
|
|
@@ -1160,6 +1688,11 @@ var init_wwr = __esm({
|
|
|
1160
1688
|
}
|
|
1161
1689
|
const xml = await res.text();
|
|
1162
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
|
+
}
|
|
1163
1696
|
return items.map((item) => ({
|
|
1164
1697
|
id: extractId(item.link),
|
|
1165
1698
|
source: "wwr",
|
|
@@ -1171,7 +1704,7 @@ var init_wwr = __esm({
|
|
|
1171
1704
|
location: "Remote",
|
|
1172
1705
|
tags: extractTags5(item),
|
|
1173
1706
|
roleType: inferRoleType(item.category),
|
|
1174
|
-
postedAt:
|
|
1707
|
+
postedAt: safeIso(item.pubDate),
|
|
1175
1708
|
applyMode: "direct",
|
|
1176
1709
|
raw: item
|
|
1177
1710
|
}));
|
|
@@ -1181,9 +1714,6 @@ var init_wwr = __esm({
|
|
|
1181
1714
|
});
|
|
1182
1715
|
|
|
1183
1716
|
// ../../packages/core/src/feeds/hn.ts
|
|
1184
|
-
function tokenize6(text) {
|
|
1185
|
-
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
1186
|
-
}
|
|
1187
1717
|
function stripHtml2(html) {
|
|
1188
1718
|
return decodeEntities(html.replace(/<p>/gi, " ").replace(/<[^>]*>/g, "")).replace(/\s+/g, " ").trim();
|
|
1189
1719
|
}
|
|
@@ -1214,7 +1744,7 @@ function parseComment(item) {
|
|
|
1214
1744
|
return null;
|
|
1215
1745
|
}
|
|
1216
1746
|
const url = extractUrl(raw) || `https://news.ycombinator.com/item?id=${item.id}`;
|
|
1217
|
-
const tags = extractTags6(raw);
|
|
1747
|
+
const tags = extractTags6(title, raw);
|
|
1218
1748
|
if (tags.length === 0) return null;
|
|
1219
1749
|
return {
|
|
1220
1750
|
id: `hn:${item.id}`,
|
|
@@ -1231,8 +1761,8 @@ function parseComment(item) {
|
|
|
1231
1761
|
raw: item
|
|
1232
1762
|
};
|
|
1233
1763
|
}
|
|
1234
|
-
function extractTags6(text) {
|
|
1235
|
-
return
|
|
1764
|
+
function extractTags6(title, text) {
|
|
1765
|
+
return extractSkillTags(title, text);
|
|
1236
1766
|
}
|
|
1237
1767
|
var ALGOLIA_SEARCH, ALGOLIA_ITEMS, hn;
|
|
1238
1768
|
var init_hn = __esm({
|
|
@@ -1240,13 +1770,14 @@ var init_hn = __esm({
|
|
|
1240
1770
|
"use strict";
|
|
1241
1771
|
init_vocabulary();
|
|
1242
1772
|
init_entities();
|
|
1773
|
+
init_http();
|
|
1243
1774
|
ALGOLIA_SEARCH = "https://hn.algolia.com/api/v1/search?query=Ask+HN%3A+Who+is+Hiring%3F&tags=story,ask_hn&hitsPerPage=1";
|
|
1244
1775
|
ALGOLIA_ITEMS = "https://hn.algolia.com/api/v1/items/";
|
|
1245
1776
|
hn = {
|
|
1246
1777
|
source: "hn",
|
|
1247
1778
|
async fetch(opts) {
|
|
1248
1779
|
const limit = opts?.limit ?? 150;
|
|
1249
|
-
const searchRes = await
|
|
1780
|
+
const searchRes = await fetchWithTimeout(ALGOLIA_SEARCH, {
|
|
1250
1781
|
headers: { Accept: "application/json" }
|
|
1251
1782
|
});
|
|
1252
1783
|
if (!searchRes.ok) {
|
|
@@ -1257,7 +1788,7 @@ var init_hn = __esm({
|
|
|
1257
1788
|
if (!story) {
|
|
1258
1789
|
throw new Error('HN: No "Who is Hiring" story found');
|
|
1259
1790
|
}
|
|
1260
|
-
const itemRes = await
|
|
1791
|
+
const itemRes = await fetchWithTimeout(`${ALGOLIA_ITEMS}${story.objectID}`, {
|
|
1261
1792
|
headers: { Accept: "application/json" }
|
|
1262
1793
|
});
|
|
1263
1794
|
if (!itemRes.ok) {
|
|
@@ -1322,7 +1853,7 @@ function authHeaders() {
|
|
|
1322
1853
|
if (token) h["Authorization"] = `Bearer ${token}`;
|
|
1323
1854
|
return h;
|
|
1324
1855
|
}
|
|
1325
|
-
function
|
|
1856
|
+
function tokenize2(text) {
|
|
1326
1857
|
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
1327
1858
|
}
|
|
1328
1859
|
function parseAmountUSD(text) {
|
|
@@ -1351,7 +1882,7 @@ function isBountyIssue(issue) {
|
|
|
1351
1882
|
async function ghJson(path) {
|
|
1352
1883
|
let res;
|
|
1353
1884
|
try {
|
|
1354
|
-
res = await
|
|
1885
|
+
res = await fetchWithTimeout(`${GITHUB_API}${path}`, { headers: authHeaders() });
|
|
1355
1886
|
} catch (err) {
|
|
1356
1887
|
console.warn(`[github-bounties] network error ${path} \u2014`, err);
|
|
1357
1888
|
return null;
|
|
@@ -1407,7 +1938,7 @@ async function fetchRepoBounties(repoFullName) {
|
|
|
1407
1938
|
const body = issue.body ? decodeEntities(issue.body) : "";
|
|
1408
1939
|
const amountUSD = parseAmountUSD(title) ?? parseAmountUSD(body) ?? await fetchCommentAmount(repoFullName, issue.number);
|
|
1409
1940
|
const labels = labelNames(issue);
|
|
1410
|
-
const tags = normalize(
|
|
1941
|
+
const tags = normalize(tokenize2([title, labels.join(" "), body.slice(0, 2e3)].join(" ")));
|
|
1411
1942
|
return {
|
|
1412
1943
|
id: `bounty:${repoFullName}#${issue.number}`,
|
|
1413
1944
|
source: "bounty",
|
|
@@ -1433,31 +1964,328 @@ async function fetchRepoBounties(repoFullName) {
|
|
|
1433
1964
|
};
|
|
1434
1965
|
}));
|
|
1435
1966
|
}
|
|
1436
|
-
|
|
1967
|
+
function repoFullNameFromApiUrl(url) {
|
|
1968
|
+
const m = url.match(/\/repos\/([^/]+)\/([^/]+)\/?$/);
|
|
1969
|
+
return m ? `${m[1]}/${m[2]}` : null;
|
|
1970
|
+
}
|
|
1971
|
+
async function searchBountyIssues() {
|
|
1972
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
1973
|
+
for (const q of SEARCH_QUERIES) {
|
|
1974
|
+
const res = await ghJson(
|
|
1975
|
+
`/search/issues?q=${encodeURIComponent(q)}&sort=created&order=desc&per_page=${SEARCH_PER_PAGE}`
|
|
1976
|
+
);
|
|
1977
|
+
for (const it of res?.items ?? []) {
|
|
1978
|
+
if (it.pull_request) continue;
|
|
1979
|
+
if (!byUrl.has(it.html_url)) byUrl.set(it.html_url, it);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
return [...byUrl.values()].sort(
|
|
1983
|
+
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1986
|
+
async function repoMetaCached(fullName) {
|
|
1987
|
+
const hit = repoMetaCache.get(fullName);
|
|
1988
|
+
if (hit !== void 0) return hit;
|
|
1989
|
+
const r = await ghJson(`/repos/${fullName}`) ?? null;
|
|
1990
|
+
repoMetaCache.set(fullName, r);
|
|
1991
|
+
return r;
|
|
1992
|
+
}
|
|
1993
|
+
async function fetchSearchBounties() {
|
|
1994
|
+
const issues = (await searchBountyIssues()).slice(0, MAX_SEARCH_ISSUES_SCANNED);
|
|
1995
|
+
const distinctRepos = [
|
|
1996
|
+
...new Set(
|
|
1997
|
+
issues.map((i) => repoFullNameFromApiUrl(i.repository_url)).filter((x) => !!x)
|
|
1998
|
+
)
|
|
1999
|
+
];
|
|
2000
|
+
for (let i = 0; i < distinctRepos.length; i += REPO_META_CONCURRENCY) {
|
|
2001
|
+
await Promise.all(distinctRepos.slice(i, i + REPO_META_CONCURRENCY).map(repoMetaCached));
|
|
2002
|
+
}
|
|
2003
|
+
const jobs = [];
|
|
2004
|
+
const perRepo = /* @__PURE__ */ new Map();
|
|
2005
|
+
for (const issue of issues) {
|
|
2006
|
+
if (jobs.length >= MAX_SEARCH_BOUNTIES) break;
|
|
2007
|
+
const fullName = repoFullNameFromApiUrl(issue.repository_url);
|
|
2008
|
+
if (!fullName) continue;
|
|
2009
|
+
if ((perRepo.get(fullName) ?? 0) >= MAX_BOUNTIES_PER_REPO) continue;
|
|
2010
|
+
const repo = await repoMetaCached(fullName);
|
|
2011
|
+
if (!repo) continue;
|
|
2012
|
+
const passes = passesMaturityGate({
|
|
2013
|
+
fullName: repo.full_name,
|
|
2014
|
+
stargazers: repo.stargazers_count,
|
|
2015
|
+
createdAt: repo.created_at,
|
|
2016
|
+
archived: repo.archived,
|
|
2017
|
+
disabled: repo.disabled
|
|
2018
|
+
});
|
|
2019
|
+
if (!passes) continue;
|
|
2020
|
+
const title = decodeEntities(issue.title).trim();
|
|
2021
|
+
const body = issue.body ? decodeEntities(issue.body) : "";
|
|
2022
|
+
const labels = labelNames(issue);
|
|
2023
|
+
let amountUSD = parseAmountUSD(title) ?? parseAmountUSD(labels.join(" ")) ?? parseAmountUSD(body);
|
|
2024
|
+
if (amountUSD == null && labels.some((n) => /💎|💰/.test(n))) {
|
|
2025
|
+
amountUSD = await fetchCommentAmount(fullName, issue.number);
|
|
2026
|
+
}
|
|
2027
|
+
if (amountUSD == null) continue;
|
|
2028
|
+
if (amountUSD > SEARCH_HIGH_VALUE_USD && repo.stargazers_count < SEARCH_HIGH_VALUE_MIN_STARS) continue;
|
|
2029
|
+
const tags = normalize(
|
|
2030
|
+
tokenize2([title, labels.join(" "), body.slice(0, 2e3)].join(" "))
|
|
2031
|
+
);
|
|
2032
|
+
perRepo.set(fullName, (perRepo.get(fullName) ?? 0) + 1);
|
|
2033
|
+
jobs.push({
|
|
2034
|
+
id: `bounty:${fullName}#${issue.number}`,
|
|
2035
|
+
source: "bounty",
|
|
2036
|
+
title,
|
|
2037
|
+
company: repo.owner.login,
|
|
2038
|
+
url: issue.html_url,
|
|
2039
|
+
remote: true,
|
|
2040
|
+
location: "Remote",
|
|
2041
|
+
tags,
|
|
2042
|
+
roleType: "freelance",
|
|
2043
|
+
postedAt: issue.created_at,
|
|
2044
|
+
applyMode: "direct",
|
|
2045
|
+
bounty: {
|
|
2046
|
+
amountUSD,
|
|
2047
|
+
estimatedEffort: effortFromAmount(amountUSD),
|
|
2048
|
+
bountySource: "github",
|
|
2049
|
+
claimUrl: issue.html_url,
|
|
2050
|
+
repoFullName: fullName,
|
|
2051
|
+
repoStars: repo.stargazers_count,
|
|
2052
|
+
issueBody: body.slice(0, 1e3) || void 0
|
|
2053
|
+
},
|
|
2054
|
+
raw: issue
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
return jobs;
|
|
2058
|
+
}
|
|
2059
|
+
var GITHUB_API, BOUNTY_LABEL_RE, SEARCH_QUERIES, SEARCH_PER_PAGE, MAX_SEARCH_BOUNTIES, MAX_SEARCH_ISSUES_SCANNED, REPO_META_CONCURRENCY, SEARCH_HIGH_VALUE_USD, SEARCH_HIGH_VALUE_MIN_STARS, repoMetaCache, githubBounties;
|
|
1437
2060
|
var init_github_bounties = __esm({
|
|
1438
2061
|
"../../packages/core/src/feeds/github-bounties.ts"() {
|
|
1439
2062
|
"use strict";
|
|
1440
2063
|
init_vocabulary();
|
|
1441
2064
|
init_entities();
|
|
1442
2065
|
init_bounty_gate();
|
|
2066
|
+
init_http();
|
|
1443
2067
|
GITHUB_API = "https://api.github.com";
|
|
1444
2068
|
BOUNTY_LABEL_RE = /bounty|reward|funded|💎|💰/i;
|
|
2069
|
+
SEARCH_QUERIES = [
|
|
2070
|
+
'label:"\u{1F48E} Bounty" type:issue state:open',
|
|
2071
|
+
// Algora-applied — highest signal
|
|
2072
|
+
"label:bounty type:issue state:open",
|
|
2073
|
+
'label:"\u{1F4B0} Bounty" type:issue state:open'
|
|
2074
|
+
];
|
|
2075
|
+
SEARCH_PER_PAGE = 100;
|
|
2076
|
+
MAX_SEARCH_BOUNTIES = 150;
|
|
2077
|
+
MAX_SEARCH_ISSUES_SCANNED = 300;
|
|
2078
|
+
REPO_META_CONCURRENCY = 15;
|
|
2079
|
+
SEARCH_HIGH_VALUE_USD = 500;
|
|
2080
|
+
SEARCH_HIGH_VALUE_MIN_STARS = 50;
|
|
2081
|
+
repoMetaCache = /* @__PURE__ */ new Map();
|
|
1445
2082
|
githubBounties = {
|
|
1446
2083
|
source: "bounty",
|
|
1447
2084
|
async fetch(opts) {
|
|
1448
|
-
const
|
|
1449
|
-
|
|
1450
|
-
|
|
2085
|
+
const allowlist = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : DEFAULT_BOUNTY_REPOS;
|
|
2086
|
+
const [searched, listed] = await Promise.all([
|
|
2087
|
+
fetchSearchBounties().catch((e) => {
|
|
2088
|
+
console.warn("[github-bounties] search discovery failed:", e);
|
|
2089
|
+
return [];
|
|
2090
|
+
}),
|
|
2091
|
+
Promise.allSettled(allowlist.map(fetchRepoBounties)).then(
|
|
2092
|
+
(settled) => settled.flatMap((r) => r.status === "fulfilled" ? r.value : [])
|
|
2093
|
+
)
|
|
2094
|
+
]);
|
|
2095
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2096
|
+
const out = [];
|
|
2097
|
+
for (const j of [...searched, ...listed]) {
|
|
2098
|
+
if (!seen.has(j.id)) {
|
|
2099
|
+
seen.add(j.id);
|
|
2100
|
+
out.push(j);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
console.info(
|
|
2104
|
+
`[github-bounties] total: ${out.length} bounties (${searched.length} search + ${listed.length} allowlist, deduped)`
|
|
2105
|
+
);
|
|
2106
|
+
return out;
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
// ../../packages/core/src/feeds/opire.ts
|
|
2113
|
+
function tokenize3(text) {
|
|
2114
|
+
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter((w) => w.length > 1);
|
|
2115
|
+
}
|
|
2116
|
+
function effortFromAmount2(usd) {
|
|
2117
|
+
if (usd == null) return void 0;
|
|
2118
|
+
if (usd < 150) return "small";
|
|
2119
|
+
if (usd < 750) return "medium";
|
|
2120
|
+
return "large";
|
|
2121
|
+
}
|
|
2122
|
+
function priceToUSD(p) {
|
|
2123
|
+
if (!p || typeof p.value !== "number") return void 0;
|
|
2124
|
+
if (p.unit === "USD_CENT") return Math.round(p.value) / 100;
|
|
2125
|
+
if (p.unit === "USD") return p.value;
|
|
2126
|
+
return void 0;
|
|
2127
|
+
}
|
|
2128
|
+
function repoFullNameFromUrl(url) {
|
|
2129
|
+
const m = url?.match(/github\.com\/([^/]+)\/([^/]+)/i);
|
|
2130
|
+
return m ? `${m[1]}/${m[2].replace(/\.git$/, "")}` : void 0;
|
|
2131
|
+
}
|
|
2132
|
+
var OPIRE_REWARDS_URL, MIN_USD, MAX_USD, MAX_OPIRE_BOUNTIES, opire;
|
|
2133
|
+
var init_opire = __esm({
|
|
2134
|
+
"../../packages/core/src/feeds/opire.ts"() {
|
|
2135
|
+
"use strict";
|
|
2136
|
+
init_vocabulary();
|
|
2137
|
+
init_http();
|
|
2138
|
+
OPIRE_REWARDS_URL = "https://api.opire.dev/rewards";
|
|
2139
|
+
MIN_USD = 25;
|
|
2140
|
+
MAX_USD = 25e3;
|
|
2141
|
+
MAX_OPIRE_BOUNTIES = 100;
|
|
2142
|
+
opire = {
|
|
2143
|
+
source: "bounty",
|
|
2144
|
+
async fetch() {
|
|
2145
|
+
let rewards;
|
|
2146
|
+
try {
|
|
2147
|
+
const res = await fetchWithTimeout(OPIRE_REWARDS_URL, {
|
|
2148
|
+
headers: { Accept: "application/json", "User-Agent": "terminalhire" }
|
|
2149
|
+
});
|
|
2150
|
+
if (!res.ok) {
|
|
2151
|
+
console.warn(`[opire] HTTP ${res.status}`);
|
|
2152
|
+
return [];
|
|
2153
|
+
}
|
|
2154
|
+
const json = await res.json();
|
|
2155
|
+
rewards = Array.isArray(json) ? json : json?.data ?? json?.items ?? [];
|
|
2156
|
+
} catch (err) {
|
|
2157
|
+
console.warn("[opire] fetch failed \u2014", err);
|
|
2158
|
+
return [];
|
|
2159
|
+
}
|
|
2160
|
+
const jobs = [];
|
|
2161
|
+
for (const r of rewards) {
|
|
2162
|
+
if (r.platform !== "GitHub") continue;
|
|
2163
|
+
if (r.project && r.project.isPublic === false) continue;
|
|
2164
|
+
const repoFullName = repoFullNameFromUrl(r.project?.url ?? r.url);
|
|
2165
|
+
if (!repoFullName) continue;
|
|
2166
|
+
const amountUSD = priceToUSD(r.pendingPrice);
|
|
2167
|
+
if (amountUSD == null || amountUSD < MIN_USD || amountUSD > MAX_USD) continue;
|
|
2168
|
+
const title = (r.title ?? "").trim();
|
|
2169
|
+
if (title.length < 4) continue;
|
|
2170
|
+
const tags = normalize([...r.programmingLanguages ?? [], ...tokenize3(title)]);
|
|
2171
|
+
jobs.push({
|
|
2172
|
+
id: `bounty:opire:${r.id}`,
|
|
2173
|
+
source: "bounty",
|
|
2174
|
+
title,
|
|
2175
|
+
company: r.organization?.name ?? repoFullName.split("/")[0],
|
|
2176
|
+
url: r.url,
|
|
2177
|
+
remote: true,
|
|
2178
|
+
location: "Remote",
|
|
2179
|
+
tags,
|
|
2180
|
+
roleType: "freelance",
|
|
2181
|
+
postedAt: Number.isFinite(r.createdAt) ? new Date(r.createdAt).toISOString() : void 0,
|
|
2182
|
+
applyMode: "direct",
|
|
2183
|
+
bounty: {
|
|
2184
|
+
amountUSD,
|
|
2185
|
+
estimatedEffort: effortFromAmount2(amountUSD),
|
|
2186
|
+
bountySource: "opire",
|
|
2187
|
+
claimUrl: r.url,
|
|
2188
|
+
repoFullName
|
|
2189
|
+
},
|
|
2190
|
+
raw: r
|
|
2191
|
+
});
|
|
2192
|
+
if (jobs.length >= MAX_OPIRE_BOUNTIES) break;
|
|
2193
|
+
}
|
|
2194
|
+
console.info(`[opire] ${jobs.length} bounties (from ${rewards.length} rewards)`);
|
|
2195
|
+
return jobs;
|
|
2196
|
+
}
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
});
|
|
2200
|
+
|
|
2201
|
+
// ../../packages/core/src/feeds/workable.ts
|
|
2202
|
+
function locationStr(loc) {
|
|
2203
|
+
if (!loc) return "";
|
|
2204
|
+
return [loc.city, loc.country].filter(Boolean).join(", ");
|
|
2205
|
+
}
|
|
2206
|
+
function isRemote(j) {
|
|
2207
|
+
return j.remote === true || (j.workplace ?? "").toLowerCase() === "remote";
|
|
2208
|
+
}
|
|
2209
|
+
function extractTags7(j) {
|
|
2210
|
+
const body = [...j.department ?? [], locationStr(j.location)].filter(Boolean).join(" ");
|
|
2211
|
+
return extractSkillTags(j.title, body);
|
|
2212
|
+
}
|
|
2213
|
+
async function fetchAccount(account) {
|
|
2214
|
+
const url = `https://apply.workable.com/api/v3/accounts/${account}/jobs`;
|
|
2215
|
+
const out = [];
|
|
2216
|
+
let token;
|
|
2217
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
2218
|
+
let res;
|
|
2219
|
+
try {
|
|
2220
|
+
res = await fetchWithTimeout(url, {
|
|
2221
|
+
method: "POST",
|
|
2222
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
2223
|
+
body: JSON.stringify(token ? { token } : {})
|
|
2224
|
+
});
|
|
2225
|
+
} catch (err) {
|
|
2226
|
+
console.warn(`[workable] ${account}: network error \u2014`, err);
|
|
2227
|
+
break;
|
|
2228
|
+
}
|
|
2229
|
+
if (!res.ok) {
|
|
2230
|
+
console.warn(`[workable] ${account}: HTTP ${res.status}`);
|
|
2231
|
+
break;
|
|
2232
|
+
}
|
|
2233
|
+
let data;
|
|
2234
|
+
try {
|
|
2235
|
+
data = await res.json();
|
|
2236
|
+
} catch (err) {
|
|
2237
|
+
console.warn(`[workable] ${account}: JSON parse error \u2014`, err);
|
|
2238
|
+
break;
|
|
2239
|
+
}
|
|
2240
|
+
const results = data.results ?? [];
|
|
2241
|
+
for (const j of results) {
|
|
2242
|
+
if (j.state && j.state !== "published") continue;
|
|
2243
|
+
out.push({
|
|
2244
|
+
id: `workable:${j.id}`,
|
|
2245
|
+
source: "workable",
|
|
2246
|
+
title: j.title,
|
|
2247
|
+
company: account,
|
|
2248
|
+
url: `https://apply.workable.com/${account}/j/${j.shortcode}/`,
|
|
2249
|
+
remote: isRemote(j),
|
|
2250
|
+
location: locationStr(j.location) || void 0,
|
|
2251
|
+
tags: extractTags7(j),
|
|
2252
|
+
roleType: "full_time",
|
|
2253
|
+
postedAt: j.published,
|
|
2254
|
+
applyMode: "direct",
|
|
2255
|
+
raw: j
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
token = data.token;
|
|
2259
|
+
if (!token || results.length === 0) break;
|
|
2260
|
+
}
|
|
2261
|
+
if (out.length > 0) console.info(`[workable] ${account}: ${out.length} jobs`);
|
|
2262
|
+
return out;
|
|
2263
|
+
}
|
|
2264
|
+
var FALLBACK_ACCOUNTS, MAX_PAGES, workable;
|
|
2265
|
+
var init_workable = __esm({
|
|
2266
|
+
"../../packages/core/src/feeds/workable.ts"() {
|
|
2267
|
+
"use strict";
|
|
2268
|
+
init_vocabulary();
|
|
2269
|
+
init_http();
|
|
2270
|
+
FALLBACK_ACCOUNTS = ["zego", "workmotion"];
|
|
2271
|
+
MAX_PAGES = 5;
|
|
2272
|
+
workable = {
|
|
2273
|
+
source: "workable",
|
|
2274
|
+
async fetch(opts) {
|
|
2275
|
+
const accounts = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : FALLBACK_ACCOUNTS;
|
|
2276
|
+
console.info(`[workable] fetching ${accounts.length} accounts: ${accounts.join(", ")}`);
|
|
2277
|
+
const results = await Promise.allSettled(accounts.map(fetchAccount));
|
|
1451
2278
|
const jobs = [];
|
|
1452
2279
|
let failures = 0;
|
|
1453
|
-
for (const r of
|
|
1454
|
-
if (r.status === "fulfilled")
|
|
1455
|
-
|
|
2280
|
+
for (const r of results) {
|
|
2281
|
+
if (r.status === "fulfilled") {
|
|
2282
|
+
jobs.push(...r.value);
|
|
2283
|
+
} else {
|
|
1456
2284
|
failures++;
|
|
1457
|
-
console.warn("[
|
|
2285
|
+
console.warn("[workable] account fetch rejected:", r.reason);
|
|
1458
2286
|
}
|
|
1459
2287
|
}
|
|
1460
|
-
console.info(`[
|
|
2288
|
+
console.info(`[workable] total: ${jobs.length} jobs, ${failures} account failures`);
|
|
1461
2289
|
return jobs;
|
|
1462
2290
|
}
|
|
1463
2291
|
};
|
|
@@ -1466,7 +2294,19 @@ var init_github_bounties = __esm({
|
|
|
1466
2294
|
|
|
1467
2295
|
// ../../packages/core/src/feeds/index.ts
|
|
1468
2296
|
async function aggregateBounties(opts) {
|
|
1469
|
-
|
|
2297
|
+
const [gh, op] = await Promise.all([
|
|
2298
|
+
githubBounties.fetch({ slugs: opts?.repos }),
|
|
2299
|
+
opire.fetch()
|
|
2300
|
+
]);
|
|
2301
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2302
|
+
const out = [];
|
|
2303
|
+
for (const j of [...gh, ...op]) {
|
|
2304
|
+
const key = j.bounty?.claimUrl ?? j.url;
|
|
2305
|
+
if (seen.has(key)) continue;
|
|
2306
|
+
seen.add(key);
|
|
2307
|
+
out.push(j);
|
|
2308
|
+
}
|
|
2309
|
+
return out;
|
|
1470
2310
|
}
|
|
1471
2311
|
function flattenTiers(t) {
|
|
1472
2312
|
return [.../* @__PURE__ */ new Set([...t.bigco, ...t.scaleup, ...t.startup])];
|
|
@@ -1475,18 +2315,20 @@ async function aggregate(opts) {
|
|
|
1475
2315
|
const ghSlugs = opts?.slugs?.["greenhouse"] ?? DEFAULT_GREENHOUSE_SLUGS;
|
|
1476
2316
|
const ashbySlugs = opts?.slugs?.["ashby"] ?? DEFAULT_ASHBY_SLUGS;
|
|
1477
2317
|
const leverSlugs = opts?.slugs?.["lever"] ?? DEFAULT_LEVER_SLUGS;
|
|
2318
|
+
const workableSlugs = opts?.slugs?.["workable"] ?? DEFAULT_WORKABLE_SLUGS;
|
|
1478
2319
|
const limit = opts?.limit ?? 150;
|
|
1479
2320
|
const settled = await Promise.allSettled([
|
|
1480
2321
|
greenhouse.fetch({ slugs: ghSlugs, limit }),
|
|
1481
2322
|
ashby.fetch({ slugs: ashbySlugs, limit }),
|
|
1482
2323
|
lever.fetch({ slugs: leverSlugs, limit }),
|
|
2324
|
+
workable.fetch({ slugs: workableSlugs, limit }),
|
|
1483
2325
|
himalayas.fetch({ limit }),
|
|
1484
2326
|
wwr.fetch({ limit }),
|
|
1485
2327
|
hn.fetch({ limit })
|
|
1486
2328
|
]);
|
|
1487
2329
|
const seen = /* @__PURE__ */ new Set();
|
|
1488
2330
|
const jobs = [];
|
|
1489
|
-
const sourceNames = ["greenhouse", "ashby", "lever", "himalayas", "wwr", "hn"];
|
|
2331
|
+
const sourceNames = ["greenhouse", "ashby", "lever", "workable", "himalayas", "wwr", "hn"];
|
|
1490
2332
|
for (let i = 0; i < settled.length; i++) {
|
|
1491
2333
|
const result = settled[i];
|
|
1492
2334
|
if (result.status === "rejected") {
|
|
@@ -1502,7 +2344,7 @@ async function aggregate(opts) {
|
|
|
1502
2344
|
}
|
|
1503
2345
|
if (opts?.includeBounties !== false) {
|
|
1504
2346
|
try {
|
|
1505
|
-
const bounties = await
|
|
2347
|
+
const bounties = await aggregateBounties({ repos: opts?.slugs?.["bounty"] });
|
|
1506
2348
|
for (const b of bounties) {
|
|
1507
2349
|
if (!seen.has(b.id)) {
|
|
1508
2350
|
seen.add(b.id);
|
|
@@ -1515,7 +2357,7 @@ async function aggregate(opts) {
|
|
|
1515
2357
|
}
|
|
1516
2358
|
return jobs;
|
|
1517
2359
|
}
|
|
1518
|
-
var FEEDS, GREENHOUSE_SLUGS_BY_TIER, ASHBY_SLUGS_BY_TIER, LEVER_SLUGS_BY_TIER, DEFAULT_GREENHOUSE_SLUGS, DEFAULT_ASHBY_SLUGS, DEFAULT_LEVER_SLUGS;
|
|
2360
|
+
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;
|
|
1519
2361
|
var init_feeds = __esm({
|
|
1520
2362
|
"../../packages/core/src/feeds/index.ts"() {
|
|
1521
2363
|
"use strict";
|
|
@@ -1526,8 +2368,10 @@ var init_feeds = __esm({
|
|
|
1526
2368
|
init_wwr();
|
|
1527
2369
|
init_hn();
|
|
1528
2370
|
init_github_bounties();
|
|
2371
|
+
init_opire();
|
|
2372
|
+
init_workable();
|
|
1529
2373
|
init_bounty_gate();
|
|
1530
|
-
FEEDS = [greenhouse, ashby, lever, himalayas, wwr, hn];
|
|
2374
|
+
FEEDS = [greenhouse, ashby, lever, workable, himalayas, wwr, hn];
|
|
1531
2375
|
GREENHOUSE_SLUGS_BY_TIER = {
|
|
1532
2376
|
bigco: [
|
|
1533
2377
|
"stripe",
|
|
@@ -1633,6 +2477,7 @@ var init_feeds = __esm({
|
|
|
1633
2477
|
DEFAULT_GREENHOUSE_SLUGS = flattenTiers(GREENHOUSE_SLUGS_BY_TIER);
|
|
1634
2478
|
DEFAULT_ASHBY_SLUGS = flattenTiers(ASHBY_SLUGS_BY_TIER);
|
|
1635
2479
|
DEFAULT_LEVER_SLUGS = flattenTiers(LEVER_SLUGS_BY_TIER);
|
|
2480
|
+
DEFAULT_WORKABLE_SLUGS = ["zego", "workmotion"];
|
|
1636
2481
|
}
|
|
1637
2482
|
});
|
|
1638
2483
|
|
|
@@ -1724,103 +2569,6 @@ var init_indexer = __esm({
|
|
|
1724
2569
|
}
|
|
1725
2570
|
});
|
|
1726
2571
|
|
|
1727
|
-
// ../../packages/core/src/github.ts
|
|
1728
|
-
function ghHeaders(token) {
|
|
1729
|
-
const headers = {
|
|
1730
|
-
Accept: "application/vnd.github+json",
|
|
1731
|
-
"X-GitHub-Api-Version": "2022-11-28"
|
|
1732
|
-
};
|
|
1733
|
-
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
1734
|
-
return headers;
|
|
1735
|
-
}
|
|
1736
|
-
async function ghFetch(path, token) {
|
|
1737
|
-
const url = `https://api.github.com${path}`;
|
|
1738
|
-
const res = await fetch(url, { headers: ghHeaders(token) });
|
|
1739
|
-
if (!res.ok) {
|
|
1740
|
-
throw new Error(`GitHub API ${path}: HTTP ${res.status} ${res.statusText}`);
|
|
1741
|
-
}
|
|
1742
|
-
return res.json();
|
|
1743
|
-
}
|
|
1744
|
-
async function fetchGitHubProfile(login, token) {
|
|
1745
|
-
const user = await ghFetch(`/users/${login}`, token);
|
|
1746
|
-
let repos = [];
|
|
1747
|
-
try {
|
|
1748
|
-
repos = await ghFetch(
|
|
1749
|
-
`/users/${login}/repos?sort=pushed&per_page=100`,
|
|
1750
|
-
token
|
|
1751
|
-
);
|
|
1752
|
-
} catch (err) {
|
|
1753
|
-
console.warn(`[github] ${login}: repos fetch failed, continuing \u2014`, err);
|
|
1754
|
-
}
|
|
1755
|
-
const langCount = {};
|
|
1756
|
-
for (const repo of repos) {
|
|
1757
|
-
if (repo.fork) continue;
|
|
1758
|
-
if (repo.language) {
|
|
1759
|
-
langCount[repo.language.toLowerCase()] = (langCount[repo.language.toLowerCase()] ?? 0) + 1;
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
const topLanguages = Object.entries(langCount).sort(([, a], [, b]) => b - a).slice(0, 10).map(([lang]) => lang);
|
|
1763
|
-
const topicSet = /* @__PURE__ */ new Set();
|
|
1764
|
-
for (const repo of repos) {
|
|
1765
|
-
if (repo.fork) continue;
|
|
1766
|
-
for (const t of repo.topics ?? []) topicSet.add(t.toLowerCase());
|
|
1767
|
-
}
|
|
1768
|
-
const topics = Array.from(topicSet).slice(0, 30);
|
|
1769
|
-
let recentPRorgs;
|
|
1770
|
-
try {
|
|
1771
|
-
const q = encodeURIComponent(
|
|
1772
|
-
`type:pr is:merged author:${login} sort:updated`
|
|
1773
|
-
);
|
|
1774
|
-
const result = await ghFetch(
|
|
1775
|
-
`/search/issues?q=${q}&per_page=30`,
|
|
1776
|
-
token
|
|
1777
|
-
);
|
|
1778
|
-
const orgs = /* @__PURE__ */ new Set();
|
|
1779
|
-
for (const item of result.items ?? []) {
|
|
1780
|
-
const orgLogin = item.repository?.owner?.login;
|
|
1781
|
-
if (orgLogin && orgLogin !== login) orgs.add(orgLogin);
|
|
1782
|
-
}
|
|
1783
|
-
if (orgs.size > 0) recentPRorgs = Array.from(orgs);
|
|
1784
|
-
} catch {
|
|
1785
|
-
}
|
|
1786
|
-
return {
|
|
1787
|
-
login: user.login,
|
|
1788
|
-
name: user.name ?? void 0,
|
|
1789
|
-
publicEmail: user.email ?? void 0,
|
|
1790
|
-
avatarUrl: user.avatar_url,
|
|
1791
|
-
accountCreatedAt: user.created_at,
|
|
1792
|
-
publicRepos: user.public_repos,
|
|
1793
|
-
followers: user.followers,
|
|
1794
|
-
topLanguages,
|
|
1795
|
-
topics,
|
|
1796
|
-
recentPRorgs
|
|
1797
|
-
};
|
|
1798
|
-
}
|
|
1799
|
-
function inferSeniority2(p) {
|
|
1800
|
-
const ageMs = Date.now() - new Date(p.accountCreatedAt).getTime();
|
|
1801
|
-
const ageYears = ageMs / (1e3 * 60 * 60 * 24 * 365.25);
|
|
1802
|
-
if (ageYears >= 9 && (p.publicRepos >= 40 || p.followers >= 500)) return "staff";
|
|
1803
|
-
if (ageYears >= 5 && (p.publicRepos >= 20 || p.followers >= 100)) return "senior";
|
|
1804
|
-
if (ageYears >= 2 && p.publicRepos >= 5) return "mid";
|
|
1805
|
-
return "junior";
|
|
1806
|
-
}
|
|
1807
|
-
function githubToFingerprint(p) {
|
|
1808
|
-
const rawTokens = [
|
|
1809
|
-
...p.topLanguages,
|
|
1810
|
-
...p.topics
|
|
1811
|
-
// recentPRorgs intentionally excluded — org names are not skill tags
|
|
1812
|
-
];
|
|
1813
|
-
const skillTags = normalize(rawTokens);
|
|
1814
|
-
const seniorityBand = inferSeniority2(p);
|
|
1815
|
-
return { skillTags, seniorityBand };
|
|
1816
|
-
}
|
|
1817
|
-
var init_github = __esm({
|
|
1818
|
-
"../../packages/core/src/github.ts"() {
|
|
1819
|
-
"use strict";
|
|
1820
|
-
init_vocabulary();
|
|
1821
|
-
}
|
|
1822
|
-
});
|
|
1823
|
-
|
|
1824
2572
|
// ../../packages/core/src/index.ts
|
|
1825
2573
|
var src_exports = {};
|
|
1826
2574
|
__export(src_exports, {
|
|
@@ -1830,22 +2578,32 @@ __export(src_exports, {
|
|
|
1830
2578
|
DEFAULT_BOUNTY_REPOS: () => DEFAULT_BOUNTY_REPOS,
|
|
1831
2579
|
DEFAULT_GREENHOUSE_SLUGS: () => DEFAULT_GREENHOUSE_SLUGS,
|
|
1832
2580
|
DEFAULT_LEVER_SLUGS: () => DEFAULT_LEVER_SLUGS,
|
|
2581
|
+
DEFAULT_WORKABLE_SLUGS: () => DEFAULT_WORKABLE_SLUGS,
|
|
1833
2582
|
EXAMPLE_BUYER: () => EXAMPLE_BUYER,
|
|
1834
2583
|
FEEDS: () => FEEDS,
|
|
1835
2584
|
GRAPH: () => GRAPH,
|
|
1836
2585
|
GREENHOUSE_SLUGS_BY_TIER: () => GREENHOUSE_SLUGS_BY_TIER,
|
|
2586
|
+
IDF_BACKGROUND: () => IDF_BACKGROUND,
|
|
1837
2587
|
LEVER_SLUGS_BY_TIER: () => LEVER_SLUGS_BY_TIER,
|
|
1838
2588
|
SYNONYMS: () => SYNONYMS,
|
|
1839
2589
|
VOCABULARY: () => VOCABULARY,
|
|
1840
2590
|
VOCAB_NODES: () => VOCAB_NODES,
|
|
2591
|
+
acceptanceCountForDomains: () => acceptanceCountForDomains,
|
|
1841
2592
|
aggregate: () => aggregate,
|
|
1842
2593
|
aggregateBounties: () => aggregateBounties,
|
|
1843
2594
|
ashby: () => ashby,
|
|
2595
|
+
bestAcceptanceDomain: () => bestAcceptanceDomain,
|
|
1844
2596
|
buildGraph: () => buildGraph,
|
|
1845
2597
|
buildIndex: () => buildIndex,
|
|
1846
2598
|
buildReason: () => buildReason,
|
|
2599
|
+
computeAcceptanceCredential: () => computeAcceptanceCredential,
|
|
2600
|
+
computeAcceptanceCredentialPublic: () => computeAcceptanceCredentialPublic,
|
|
2601
|
+
coreTagsFromTitle: () => coreTagsFromTitle,
|
|
2602
|
+
deriveResumeTrend: () => deriveResumeTrend,
|
|
1847
2603
|
expandWeighted: () => expandWeighted,
|
|
2604
|
+
extractSkillTags: () => extractSkillTags,
|
|
1848
2605
|
fetchGitHubProfile: () => fetchGitHubProfile,
|
|
2606
|
+
fetchRepoRecency: () => fetchRepoRecency,
|
|
1849
2607
|
flattenTiers: () => flattenTiers,
|
|
1850
2608
|
getBuyer: () => getBuyer,
|
|
1851
2609
|
githubBounties: () => githubBounties,
|
|
@@ -1856,11 +2614,14 @@ __export(src_exports, {
|
|
|
1856
2614
|
isBounty: () => isBounty,
|
|
1857
2615
|
lever: () => lever,
|
|
1858
2616
|
loadPartnerRoles: () => loadPartnerRoles,
|
|
2617
|
+
looksLikeEngRole: () => looksLikeEngRole,
|
|
1859
2618
|
match: () => match,
|
|
1860
|
-
matchOne: () => matchOne,
|
|
1861
2619
|
normalize: () => normalize,
|
|
2620
|
+
opire: () => opire,
|
|
1862
2621
|
passesMaturityGate: () => passesMaturityGate,
|
|
2622
|
+
tokenize: () => tokenize,
|
|
1863
2623
|
validateGraph: () => validateGraph,
|
|
2624
|
+
workable: () => workable,
|
|
1864
2625
|
wwr: () => wwr
|
|
1865
2626
|
});
|
|
1866
2627
|
var init_src = __esm({
|
|
@@ -2131,7 +2892,7 @@ async function run() {
|
|
|
2131
2892
|
}
|
|
2132
2893
|
async function runLogin() {
|
|
2133
2894
|
const { runDeviceFlow: runDeviceFlow2, readGitHubToken: readGitHubToken2 } = await Promise.resolve().then(() => (init_github_auth(), github_auth_exports));
|
|
2134
|
-
const { fetchGitHubProfile: fetchGitHubProfile2, githubToFingerprint: githubToFingerprint2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
2895
|
+
const { fetchGitHubProfile: fetchGitHubProfile2, githubToFingerprint: githubToFingerprint2, computeAcceptanceCredential: computeAcceptanceCredential2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
2135
2896
|
const { readProfile: readProfile2, writeProfile: writeProfile2, accumulateGitHubTags: accumulateGitHubTags2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
|
|
2136
2897
|
console.log("");
|
|
2137
2898
|
console.log(" terminalhire \u2014 Sign in with GitHub");
|
|
@@ -2147,7 +2908,7 @@ async function runLogin() {
|
|
|
2147
2908
|
console.log(`
|
|
2148
2909
|
Fetching public profile for @${login}...`);
|
|
2149
2910
|
let ghProfile;
|
|
2150
|
-
if (process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["
|
|
2911
|
+
if (process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["JPI_GITHUB_MOCK"] === "1") {
|
|
2151
2912
|
const { createRequire: createRequire2 } = await import("module");
|
|
2152
2913
|
const { fileURLToPath: fileURLToPath7 } = await import("url");
|
|
2153
2914
|
const { join: join15, dirname: dirname3 } = await import("path");
|
|
@@ -2173,6 +2934,15 @@ async function runLogin() {
|
|
|
2173
2934
|
topLanguages: ghProfile.topLanguages.slice(0, 5),
|
|
2174
2935
|
publicRepos: ghProfile.publicRepos
|
|
2175
2936
|
};
|
|
2937
|
+
const isMock = process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["JPI_GITHUB_MOCK"] === "1";
|
|
2938
|
+
if (!isMock) {
|
|
2939
|
+
try {
|
|
2940
|
+
console.log(" Computing proof-of-work acceptance credential...");
|
|
2941
|
+
profile.acceptance = await computeAcceptanceCredential2(ghProfile.login, token);
|
|
2942
|
+
} catch (err) {
|
|
2943
|
+
if (process.env["DEBUG"]) console.warn(" [acceptance] credential compute failed:", err);
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2176
2946
|
await writeProfile2(profile);
|
|
2177
2947
|
console.log("");
|
|
2178
2948
|
console.log(" GitHub profile merged into local encrypted profile:");
|
|
@@ -2189,6 +2959,9 @@ async function runLogin() {
|
|
|
2189
2959
|
if (fragment.skillTags.length === 0) {
|
|
2190
2960
|
console.log(" (No matching vocabulary tags found in public repos/topics)");
|
|
2191
2961
|
}
|
|
2962
|
+
if (profile.acceptance?.status === "ok" && profile.acceptance.qualifyingTotal > 0) {
|
|
2963
|
+
console.log(` Proof-of-work: ${profile.acceptance.qualifyingTotal} merged PR${profile.acceptance.qualifyingTotal === 1 ? "" : "s"} into external repos`);
|
|
2964
|
+
}
|
|
2192
2965
|
console.log("");
|
|
2193
2966
|
console.log(" Profile updated at ~/.terminalhire/profile.enc (encrypted at rest)");
|
|
2194
2967
|
console.log(" GitHub data stays on your machine unless you consent to share it in a lead.");
|
|
@@ -2210,10 +2983,18 @@ async function runLogout() {
|
|
|
2210
2983
|
}
|
|
2211
2984
|
await deleteGitHubToken2();
|
|
2212
2985
|
const profile = await readProfile2();
|
|
2986
|
+
let changed = false;
|
|
2213
2987
|
if (profile.github) {
|
|
2214
2988
|
delete profile.github;
|
|
2989
|
+
changed = true;
|
|
2990
|
+
}
|
|
2991
|
+
if (profile.acceptance) {
|
|
2992
|
+
delete profile.acceptance;
|
|
2993
|
+
changed = true;
|
|
2994
|
+
}
|
|
2995
|
+
if (changed) {
|
|
2215
2996
|
await writeProfile2(profile);
|
|
2216
|
-
console.log("\n GitHub identity cleared from local profile.");
|
|
2997
|
+
console.log("\n GitHub identity + proof-of-work credential cleared from local profile.");
|
|
2217
2998
|
}
|
|
2218
2999
|
console.log(" GitHub token deleted from ~/.terminalhire/github-token.enc");
|
|
2219
3000
|
console.log(" Skill tags accumulated from GitHub remain in your profile.");
|
|
@@ -2336,7 +3117,7 @@ function linkTitle(title, url) {
|
|
|
2336
3117
|
return url ? `${title} (${url})` : title;
|
|
2337
3118
|
}
|
|
2338
3119
|
function printResult(i, result) {
|
|
2339
|
-
const { job, score, matchedTags, reason } = result;
|
|
3120
|
+
const { job, score, matchedTags, reason, acceptance } = result;
|
|
2340
3121
|
const comp = formatComp(job);
|
|
2341
3122
|
const remote = job.remote ? "remote" : job.location ?? "onsite";
|
|
2342
3123
|
const compStr = comp ? ` \xB7 ${comp}` : "";
|
|
@@ -2348,6 +3129,9 @@ ${i + 1}. ${titleStr} \u2014 ${job.company}${mode}`);
|
|
|
2348
3129
|
console.log(` ${remote}${compStr} \xB7 ${job.roleType} \xB7 score: ${formatScore(score)}`);
|
|
2349
3130
|
console.log(` ${reason}`);
|
|
2350
3131
|
console.log(` Tags matched: ${matchedTags.slice(0, 5).join(", ")}`);
|
|
3132
|
+
if (acceptance && acceptance.status === "ok" && acceptance.count > 0) {
|
|
3133
|
+
console.log(` \u2713 proof-of-work: ${acceptance.count} merged PR${acceptance.count === 1 ? "" : "s"} into external ${acceptance.domain} repos`);
|
|
3134
|
+
}
|
|
2351
3135
|
if (job.applyMode === "direct") {
|
|
2352
3136
|
console.log(` Apply: ${job.url}`);
|
|
2353
3137
|
} else {
|
|
@@ -2405,7 +3189,9 @@ async function run2() {
|
|
|
2405
3189
|
}
|
|
2406
3190
|
const fp = profileToFingerprint2(profile);
|
|
2407
3191
|
if (REMOTE_ONLY) fp.prefs = { ...fp.prefs, remoteOnly: true };
|
|
2408
|
-
const results = match2(fp, jobs, SHOW_ALL ? jobs.length : LIMIT)
|
|
3192
|
+
const results = match2(fp, jobs, SHOW_ALL ? jobs.length : LIMIT, Date.now(), {
|
|
3193
|
+
acceptance: profile.acceptance
|
|
3194
|
+
});
|
|
2409
3195
|
try {
|
|
2410
3196
|
const cacheRaw = readFileSync4(INDEX_CACHE_FILE, "utf8");
|
|
2411
3197
|
const cacheEntry = JSON.parse(cacheRaw);
|
|
@@ -3193,7 +3979,7 @@ function buildContextVerbs(topMatches, sessionTags) {
|
|
|
3193
3979
|
}
|
|
3194
3980
|
const list = Array.isArray(topMatches) ? topMatches : [];
|
|
3195
3981
|
const hasBounty = list.some((m) => m && m.source === "bounty");
|
|
3196
|
-
if (hasBounty) headers.
|
|
3982
|
+
if (hasBounty) headers.push(`\u2726 Roles + \u{1F48E} paid bounties in your stack \u2014 link below`);
|
|
3197
3983
|
return headers;
|
|
3198
3984
|
}
|
|
3199
3985
|
function buildSpinnerPool(topMatches, max = 6, opts = {}) {
|
|
@@ -3285,8 +4071,8 @@ function buildTips(topMatches, baseUrl, max = 8) {
|
|
|
3285
4071
|
let bi = 0;
|
|
3286
4072
|
let ri = 0;
|
|
3287
4073
|
while (bi < bountyQ.length || ri < roleQ.length) {
|
|
3288
|
-
if (bi < bountyQ.length) ordered.push(bountyQ[bi++]);
|
|
3289
4074
|
if (ri < roleQ.length) ordered.push(roleQ[ri++]);
|
|
4075
|
+
if (bi < bountyQ.length) ordered.push(bountyQ[bi++]);
|
|
3290
4076
|
}
|
|
3291
4077
|
for (const m of ordered) {
|
|
3292
4078
|
if (!m || !m.title || !m.company || !m.id) continue;
|