terminalhire 0.3.5 → 0.4.0
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 +508 -178
- package/dist/bin/jpi-dispatch.js +538 -183
- package/dist/bin/jpi-jobs.js +515 -180
- package/dist/bin/jpi-learn.js +53 -10
- package/dist/bin/jpi-login.js +531 -181
- package/dist/bin/jpi-profile.js +53 -10
- package/dist/bin/jpi-refresh.js +508 -178
- package/dist/bin/jpi-save.js +53 -10
- package/dist/bin/jpi-sync.js +53 -10
- package/dist/src/profile.js +28 -2
- package/dist/src/signal.js +28 -2
- 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 }] },
|
|
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
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 }] },
|
|
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"] },
|
|
@@ -540,6 +540,207 @@ var init_types2 = __esm({
|
|
|
540
540
|
}
|
|
541
541
|
});
|
|
542
542
|
|
|
543
|
+
// ../../packages/core/src/vocab/extract.ts
|
|
544
|
+
function tokenize(text) {
|
|
545
|
+
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
546
|
+
}
|
|
547
|
+
function looksLikeEngRole(title) {
|
|
548
|
+
return !NON_ENG_TITLE.test(title) && ENG_INTENT.test(title);
|
|
549
|
+
}
|
|
550
|
+
function resolveToken(token) {
|
|
551
|
+
const tryOne = (t) => {
|
|
552
|
+
if (GRAPH.ids.has(t)) return { id: t, viaSynonym: false };
|
|
553
|
+
const mapped = GRAPH.synonyms.get(t);
|
|
554
|
+
return mapped ? { id: mapped, viaSynonym: true } : null;
|
|
555
|
+
};
|
|
556
|
+
return tryOne(token) ?? tryOne(token.replace(/^[.\-+#]+|[.\-+#]+$/g, ""));
|
|
557
|
+
}
|
|
558
|
+
function extractSkillTags(title, body = "") {
|
|
559
|
+
if (!looksLikeEngRole(title)) return [];
|
|
560
|
+
const text = `${title}
|
|
561
|
+
${body}`;
|
|
562
|
+
const tokens = tokenize(text);
|
|
563
|
+
const ids = /* @__PURE__ */ new Set();
|
|
564
|
+
const ambiguousPending = /* @__PURE__ */ new Set();
|
|
565
|
+
for (const tok of tokens) {
|
|
566
|
+
const r = resolveToken(tok);
|
|
567
|
+
if (!r) continue;
|
|
568
|
+
if (NON_EXTRACTABLE.has(r.id)) continue;
|
|
569
|
+
if (SYNONYM_ONLY.has(r.id) && !r.viaSynonym) continue;
|
|
570
|
+
const cue = AMBIGUOUS[r.id];
|
|
571
|
+
if (cue) {
|
|
572
|
+
if (cue.test(text)) ids.add(r.id);
|
|
573
|
+
else ambiguousPending.add(r.id);
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
ids.add(r.id);
|
|
577
|
+
}
|
|
578
|
+
const hardCount = [...ids].filter((id) => !SOFT_DOMAIN.has(id)).length;
|
|
579
|
+
if (hardCount >= 2) for (const id of ambiguousPending) ids.add(id);
|
|
580
|
+
return [...ids];
|
|
581
|
+
}
|
|
582
|
+
function coreTagsFromTitle(title) {
|
|
583
|
+
return extractSkillTags(title, "").filter((t) => !SOFT_DOMAIN.has(t));
|
|
584
|
+
}
|
|
585
|
+
var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, ENG_INTENT, NON_ENG_TITLE;
|
|
586
|
+
var init_extract = __esm({
|
|
587
|
+
"../../packages/core/src/vocab/extract.ts"() {
|
|
588
|
+
"use strict";
|
|
589
|
+
init_vocab();
|
|
590
|
+
SOFT_DOMAIN = /* @__PURE__ */ new Set([
|
|
591
|
+
"frontend",
|
|
592
|
+
"backend",
|
|
593
|
+
"devops",
|
|
594
|
+
"security",
|
|
595
|
+
"payments",
|
|
596
|
+
"billing",
|
|
597
|
+
"microservices",
|
|
598
|
+
"caching",
|
|
599
|
+
"search",
|
|
600
|
+
"observability",
|
|
601
|
+
"monitoring",
|
|
602
|
+
"testing",
|
|
603
|
+
"accessibility",
|
|
604
|
+
"seo",
|
|
605
|
+
"performance",
|
|
606
|
+
"realtime",
|
|
607
|
+
"authentication",
|
|
608
|
+
"api-design"
|
|
609
|
+
]);
|
|
610
|
+
SYNONYM_ONLY = /* @__PURE__ */ new Set(["performance", "security", "seo"]);
|
|
611
|
+
NON_EXTRACTABLE = /* @__PURE__ */ new Set(["payments", "billing"]);
|
|
612
|
+
for (const id of SYNONYM_ONLY) {
|
|
613
|
+
if (!SOFT_DOMAIN.has(id)) throw new Error(`extract: SYNONYM_ONLY "${id}" not in SOFT_DOMAIN`);
|
|
614
|
+
}
|
|
615
|
+
AMBIGUOUS = {
|
|
616
|
+
// Accept "go" with an ecosystem cue OR an explicit-skill phrasing ("Go developer",
|
|
617
|
+
// "in Go", "experience with Go"). Rejects prose: "ready to go", "go above", "go live".
|
|
618
|
+
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,
|
|
619
|
+
r: /\b(rstudio|tidyverse|ggplot|shiny|dplyr|cran|r-lang|rlang)\b/i,
|
|
620
|
+
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
|
|
621
|
+
};
|
|
622
|
+
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;
|
|
623
|
+
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;
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// ../../packages/core/src/vocab/idf-background.ts
|
|
628
|
+
var IDF_BACKGROUND;
|
|
629
|
+
var init_idf_background = __esm({
|
|
630
|
+
"../../packages/core/src/vocab/idf-background.ts"() {
|
|
631
|
+
"use strict";
|
|
632
|
+
IDF_BACKGROUND = {
|
|
633
|
+
N: 244,
|
|
634
|
+
df: {
|
|
635
|
+
"backend": 71,
|
|
636
|
+
"python": 57,
|
|
637
|
+
"monitoring": 44,
|
|
638
|
+
"nextjs": 40,
|
|
639
|
+
"testing": 40,
|
|
640
|
+
"observability": 38,
|
|
641
|
+
"llm": 38,
|
|
642
|
+
"go": 36,
|
|
643
|
+
"aws": 36,
|
|
644
|
+
"react": 33,
|
|
645
|
+
"frontend": 30,
|
|
646
|
+
"ml": 28,
|
|
647
|
+
"mobile": 24,
|
|
648
|
+
"realtime": 24,
|
|
649
|
+
"typescript": 23,
|
|
650
|
+
"devops": 22,
|
|
651
|
+
"kubernetes": 22,
|
|
652
|
+
"javascript": 21,
|
|
653
|
+
"java": 20,
|
|
654
|
+
"rag": 20,
|
|
655
|
+
"api-design": 20,
|
|
656
|
+
"linux": 19,
|
|
657
|
+
"postgresql": 19,
|
|
658
|
+
"search": 17,
|
|
659
|
+
"azure": 16,
|
|
660
|
+
"snowflake": 15,
|
|
661
|
+
"spark": 15,
|
|
662
|
+
"kotlin": 14,
|
|
663
|
+
"gcp": 14,
|
|
664
|
+
"accessibility": 14,
|
|
665
|
+
"nodejs": 14,
|
|
666
|
+
"graphql": 14,
|
|
667
|
+
"airflow": 14,
|
|
668
|
+
"docker": 14,
|
|
669
|
+
"ci-cd": 13,
|
|
670
|
+
"android": 12,
|
|
671
|
+
"cpp": 12,
|
|
672
|
+
"gitlab-ci": 11,
|
|
673
|
+
"anthropic": 11,
|
|
674
|
+
"terraform": 11,
|
|
675
|
+
"mysql": 11,
|
|
676
|
+
"r": 10,
|
|
677
|
+
"dbt": 9,
|
|
678
|
+
"langchain": 9,
|
|
679
|
+
"pytorch": 9,
|
|
680
|
+
"ruby": 9,
|
|
681
|
+
"rails": 9,
|
|
682
|
+
"cloudflare": 7,
|
|
683
|
+
"datadog": 7,
|
|
684
|
+
"css": 7,
|
|
685
|
+
"ansible": 7,
|
|
686
|
+
"openai": 6,
|
|
687
|
+
"kafka": 6,
|
|
688
|
+
"rust": 5,
|
|
689
|
+
"grpc": 5,
|
|
690
|
+
"microservices": 5,
|
|
691
|
+
"serverless": 5,
|
|
692
|
+
"scala": 5,
|
|
693
|
+
"prometheus": 5,
|
|
694
|
+
"grafana": 5,
|
|
695
|
+
"php": 5,
|
|
696
|
+
"redis": 5,
|
|
697
|
+
"huggingface": 4,
|
|
698
|
+
"pandas": 4,
|
|
699
|
+
"scikit-learn": 4,
|
|
700
|
+
"html": 4,
|
|
701
|
+
"ios": 4,
|
|
702
|
+
"authentication": 4,
|
|
703
|
+
"vue": 4,
|
|
704
|
+
"mlops": 3,
|
|
705
|
+
"spring": 3,
|
|
706
|
+
"mongodb": 3,
|
|
707
|
+
"csharp": 3,
|
|
708
|
+
"swift": 2,
|
|
709
|
+
"caching": 2,
|
|
710
|
+
"haskell": 2,
|
|
711
|
+
"pulumi": 2,
|
|
712
|
+
"argocd": 2,
|
|
713
|
+
"tensorflow": 2,
|
|
714
|
+
"express": 2,
|
|
715
|
+
"elasticsearch": 2,
|
|
716
|
+
"clickhouse": 2,
|
|
717
|
+
"nestjs": 2,
|
|
718
|
+
"vite": 2,
|
|
719
|
+
"svelte": 2,
|
|
720
|
+
"phoenix": 2,
|
|
721
|
+
"angular": 2,
|
|
722
|
+
"django": 2,
|
|
723
|
+
"dotnet": 2,
|
|
724
|
+
"elixir": 2,
|
|
725
|
+
"bun": 1,
|
|
726
|
+
"oauth": 1,
|
|
727
|
+
"dynamodb": 1,
|
|
728
|
+
"helm": 1,
|
|
729
|
+
"playwright": 1,
|
|
730
|
+
"cypress": 1,
|
|
731
|
+
"jest": 1,
|
|
732
|
+
"mocha": 1,
|
|
733
|
+
"typeorm": 1,
|
|
734
|
+
"tailwind": 1,
|
|
735
|
+
"prisma": 1,
|
|
736
|
+
"expo": 1,
|
|
737
|
+
"rabbitmq": 1,
|
|
738
|
+
"redux": 1
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
|
|
543
744
|
// ../../packages/core/src/vocab/index.ts
|
|
544
745
|
function normalize(tokens) {
|
|
545
746
|
const result = /* @__PURE__ */ new Set();
|
|
@@ -576,6 +777,8 @@ var init_vocab = __esm({
|
|
|
576
777
|
init_types2();
|
|
577
778
|
init_closure();
|
|
578
779
|
init_graph_data();
|
|
780
|
+
init_extract();
|
|
781
|
+
init_idf_background();
|
|
579
782
|
GRAPH = buildGraph(VOCAB_NODES);
|
|
580
783
|
VOCABULARY = [...GRAPH.ids];
|
|
581
784
|
SYNONYMS = Object.fromEntries(GRAPH.synonyms);
|
|
@@ -590,23 +793,250 @@ var init_vocabulary = __esm({
|
|
|
590
793
|
}
|
|
591
794
|
});
|
|
592
795
|
|
|
593
|
-
// ../../packages/core/src/
|
|
594
|
-
function
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
796
|
+
// ../../packages/core/src/github.ts
|
|
797
|
+
function ghHeaders(token) {
|
|
798
|
+
const headers = {
|
|
799
|
+
Accept: "application/vnd.github+json",
|
|
800
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
801
|
+
};
|
|
802
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
803
|
+
return headers;
|
|
804
|
+
}
|
|
805
|
+
async function ghFetch(path, token) {
|
|
806
|
+
const url = `https://api.github.com${path}`;
|
|
807
|
+
const res = await fetch(url, { headers: ghHeaders(token) });
|
|
808
|
+
if (!res.ok) {
|
|
809
|
+
throw new Error(`GitHub API ${path}: HTTP ${res.status} ${res.statusText}`);
|
|
810
|
+
}
|
|
811
|
+
return res.json();
|
|
812
|
+
}
|
|
813
|
+
async function fetchGitHubProfile(login, token) {
|
|
814
|
+
const user = await ghFetch(`/users/${login}`, token);
|
|
815
|
+
let repos = [];
|
|
816
|
+
try {
|
|
817
|
+
repos = await ghFetch(
|
|
818
|
+
`/users/${login}/repos?sort=pushed&per_page=100`,
|
|
819
|
+
token
|
|
820
|
+
);
|
|
821
|
+
} catch (err) {
|
|
822
|
+
console.warn(`[github] ${login}: repos fetch failed, continuing \u2014`, err);
|
|
823
|
+
}
|
|
824
|
+
const langCount = {};
|
|
825
|
+
for (const repo of repos) {
|
|
826
|
+
if (repo.fork) continue;
|
|
827
|
+
if (repo.language) {
|
|
828
|
+
langCount[repo.language.toLowerCase()] = (langCount[repo.language.toLowerCase()] ?? 0) + 1;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
const topLanguages = Object.entries(langCount).sort(([, a], [, b]) => b - a).slice(0, 10).map(([lang]) => lang);
|
|
832
|
+
const topicSet = /* @__PURE__ */ new Set();
|
|
833
|
+
for (const repo of repos) {
|
|
834
|
+
if (repo.fork) continue;
|
|
835
|
+
for (const t of repo.topics ?? []) topicSet.add(t.toLowerCase());
|
|
836
|
+
}
|
|
837
|
+
const topics = Array.from(topicSet).slice(0, 30);
|
|
838
|
+
let recentPRorgs;
|
|
839
|
+
try {
|
|
840
|
+
const q = encodeURIComponent(
|
|
841
|
+
`type:pr is:merged author:${login} sort:updated`
|
|
842
|
+
);
|
|
843
|
+
const result = await ghFetch(
|
|
844
|
+
`/search/issues?q=${q}&per_page=30`,
|
|
845
|
+
token
|
|
846
|
+
);
|
|
847
|
+
const orgs = /* @__PURE__ */ new Set();
|
|
848
|
+
for (const item of result.items ?? []) {
|
|
849
|
+
const orgLogin = item.repository?.owner?.login;
|
|
850
|
+
if (orgLogin && orgLogin !== login) orgs.add(orgLogin);
|
|
851
|
+
}
|
|
852
|
+
if (orgs.size > 0) recentPRorgs = Array.from(orgs);
|
|
853
|
+
} catch {
|
|
854
|
+
}
|
|
855
|
+
return {
|
|
856
|
+
login: user.login,
|
|
857
|
+
name: user.name ?? void 0,
|
|
858
|
+
publicEmail: user.email ?? void 0,
|
|
859
|
+
avatarUrl: user.avatar_url,
|
|
860
|
+
accountCreatedAt: user.created_at,
|
|
861
|
+
publicRepos: user.public_repos,
|
|
862
|
+
followers: user.followers,
|
|
863
|
+
topLanguages,
|
|
864
|
+
topics,
|
|
865
|
+
recentPRorgs
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
function inferSeniority(p) {
|
|
869
|
+
const ageMs = Date.now() - new Date(p.accountCreatedAt).getTime();
|
|
870
|
+
const ageYears = ageMs / (1e3 * 60 * 60 * 24 * 365.25);
|
|
871
|
+
if (ageYears >= 9 && (p.publicRepos >= 40 || p.followers >= 500)) return "staff";
|
|
872
|
+
if (ageYears >= 5 && (p.publicRepos >= 20 || p.followers >= 100)) return "senior";
|
|
873
|
+
if (ageYears >= 2 && p.publicRepos >= 5) return "mid";
|
|
874
|
+
return "junior";
|
|
875
|
+
}
|
|
876
|
+
function githubToFingerprint(p) {
|
|
877
|
+
const rawTokens = [
|
|
878
|
+
...p.topLanguages,
|
|
879
|
+
...p.topics
|
|
880
|
+
// recentPRorgs intentionally excluded — org names are not skill tags
|
|
881
|
+
];
|
|
882
|
+
const skillTags = normalize(rawTokens);
|
|
883
|
+
const seniorityBand = inferSeniority(p);
|
|
884
|
+
return { skillTags, seniorityBand };
|
|
885
|
+
}
|
|
886
|
+
async function ghFetchRaw(path, token) {
|
|
887
|
+
return fetch(`https://api.github.com${path}`, { headers: ghHeaders(token) });
|
|
888
|
+
}
|
|
889
|
+
function parseRepoUrl(repoUrl) {
|
|
890
|
+
const m = repoUrl.match(/\/repos\/([^/]+)\/([^/]+)\/?$/);
|
|
891
|
+
return m ? { owner: m[1], name: m[2] } : null;
|
|
892
|
+
}
|
|
893
|
+
function isTrivialPRTitle(title) {
|
|
894
|
+
return TRIVIAL_PR_TITLE.test(title);
|
|
895
|
+
}
|
|
896
|
+
async function fetchOwnedOrgs(token) {
|
|
897
|
+
try {
|
|
898
|
+
const memberships = await ghFetch(`/user/memberships/orgs?per_page=100`, token);
|
|
899
|
+
return new Set(
|
|
900
|
+
memberships.filter((m) => m.role === "admin").map((m) => m.organization.login.toLowerCase())
|
|
901
|
+
);
|
|
902
|
+
} catch {
|
|
903
|
+
return /* @__PURE__ */ new Set();
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
async function repoContributorCount(owner, name, token) {
|
|
907
|
+
try {
|
|
908
|
+
const res = await ghFetchRaw(
|
|
909
|
+
`/repos/${owner}/${name}/contributors?per_page=1&anon=false`,
|
|
910
|
+
token
|
|
911
|
+
);
|
|
912
|
+
if (!res.ok) return void 0;
|
|
913
|
+
const link = res.headers.get("link");
|
|
914
|
+
const m = link?.match(/[?&]page=(\d+)>;\s*rel="last"/);
|
|
915
|
+
if (m) return Number(m[1]);
|
|
916
|
+
const body = await res.json();
|
|
917
|
+
return Array.isArray(body) ? body.length : 0;
|
|
918
|
+
} catch {
|
|
919
|
+
return void 0;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
async function fetchRepoMeta(owner, name, token, cache) {
|
|
923
|
+
const key = `${owner}/${name}`.toLowerCase();
|
|
924
|
+
const cached = cache.get(key);
|
|
925
|
+
if (cached !== void 0) return cached;
|
|
926
|
+
let meta = null;
|
|
927
|
+
try {
|
|
928
|
+
const r = await ghFetch(`/repos/${owner}/${name}`, token);
|
|
929
|
+
const contributors = await repoContributorCount(owner, name, token);
|
|
930
|
+
meta = {
|
|
931
|
+
stars: r.stargazers_count ?? 0,
|
|
932
|
+
archived: !!r.archived,
|
|
933
|
+
fork: !!r.fork,
|
|
934
|
+
language: r.language ?? null,
|
|
935
|
+
topics: r.topics ?? [],
|
|
936
|
+
contributors
|
|
937
|
+
};
|
|
938
|
+
} catch {
|
|
939
|
+
meta = null;
|
|
940
|
+
}
|
|
941
|
+
cache.set(key, meta);
|
|
942
|
+
return meta;
|
|
943
|
+
}
|
|
944
|
+
async function computeAcceptanceCredential(login, token, cache = /* @__PURE__ */ new Map()) {
|
|
945
|
+
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
|
+
const loginLc = login.toLowerCase();
|
|
955
|
+
let items;
|
|
956
|
+
try {
|
|
957
|
+
const q = encodeURIComponent(`type:pr is:merged author:${login} -user:${login} sort:updated`);
|
|
958
|
+
const res = await ghFetch(
|
|
959
|
+
`/search/issues?q=${q}&per_page=${CANDIDATE_PR_PAGE}`,
|
|
960
|
+
token
|
|
961
|
+
);
|
|
962
|
+
items = res.items ?? [];
|
|
963
|
+
} catch (err) {
|
|
964
|
+
const msg = String(err);
|
|
965
|
+
return empty(/HTTP 403|HTTP 429|rate limit/i.test(msg) ? "rate-limited" : "failed");
|
|
966
|
+
}
|
|
967
|
+
const byDomain = {};
|
|
968
|
+
let qualifyingTotal = 0;
|
|
969
|
+
for (const item of items) {
|
|
970
|
+
const repo = parseRepoUrl(item.repository_url);
|
|
971
|
+
if (!repo) continue;
|
|
972
|
+
const ownerLc = repo.owner.toLowerCase();
|
|
973
|
+
if (ownerLc === loginLc) continue;
|
|
974
|
+
if (ownedOrgs.has(ownerLc)) continue;
|
|
975
|
+
if (isTrivialPRTitle(item.title)) continue;
|
|
976
|
+
const meta = await fetchRepoMeta(repo.owner, repo.name, token, cache);
|
|
977
|
+
if (!meta) continue;
|
|
978
|
+
if (meta.archived || meta.fork) continue;
|
|
979
|
+
if (meta.stars < MIN_STARS) continue;
|
|
980
|
+
if (meta.contributors !== void 0 && meta.contributors < MIN_CONTRIBUTORS) continue;
|
|
981
|
+
qualifyingTotal += 1;
|
|
982
|
+
const mergedAt = item.pull_request?.merged_at ?? item.closed_at ?? item.created_at;
|
|
983
|
+
const rawDomains = [meta.language ?? "", ...meta.topics].filter(Boolean);
|
|
984
|
+
for (const d of new Set(normalize(rawDomains))) {
|
|
985
|
+
const b = byDomain[d] ?? (byDomain[d] = { mergedPRs: 0, distinctOrgs: 0, lastMergedAt: mergedAt, orgs: /* @__PURE__ */ new Set() });
|
|
986
|
+
b.mergedPRs += 1;
|
|
987
|
+
b.orgs.add(ownerLc);
|
|
988
|
+
if (mergedAt > b.lastMergedAt) b.lastMergedAt = mergedAt;
|
|
601
989
|
}
|
|
602
990
|
}
|
|
603
|
-
const
|
|
604
|
-
for (const [
|
|
605
|
-
|
|
991
|
+
const finalDomains = {};
|
|
992
|
+
for (const [d, b] of Object.entries(byDomain)) {
|
|
993
|
+
finalDomains[d] = {
|
|
994
|
+
mergedPRs: b.mergedPRs,
|
|
995
|
+
distinctOrgs: b.orgs.size,
|
|
996
|
+
lastMergedAt: b.lastMergedAt
|
|
997
|
+
};
|
|
606
998
|
}
|
|
607
|
-
return
|
|
999
|
+
return { status: "ok", byDomain: finalDomains, qualifyingTotal, computedAt };
|
|
608
1000
|
}
|
|
609
|
-
function
|
|
1001
|
+
function acceptanceCountForDomains(cred, domains) {
|
|
1002
|
+
if (cred.status !== "ok") return 0;
|
|
1003
|
+
let max = 0;
|
|
1004
|
+
for (const d of domains) {
|
|
1005
|
+
const c = cred.byDomain[d]?.mergedPRs ?? 0;
|
|
1006
|
+
if (c > max) max = c;
|
|
1007
|
+
}
|
|
1008
|
+
return max;
|
|
1009
|
+
}
|
|
1010
|
+
function bestAcceptanceDomain(cred, domains) {
|
|
1011
|
+
if (cred.status !== "ok") return null;
|
|
1012
|
+
let best = null;
|
|
1013
|
+
for (const d of domains) {
|
|
1014
|
+
const count = cred.byDomain[d]?.mergedPRs ?? 0;
|
|
1015
|
+
if (count > 0 && (best === null || count > best.count)) best = { domain: d, count };
|
|
1016
|
+
}
|
|
1017
|
+
return best;
|
|
1018
|
+
}
|
|
1019
|
+
var MIN_STARS, MIN_CONTRIBUTORS, CANDIDATE_PR_PAGE, TRIVIAL_PR_TITLE;
|
|
1020
|
+
var init_github = __esm({
|
|
1021
|
+
"../../packages/core/src/github.ts"() {
|
|
1022
|
+
"use strict";
|
|
1023
|
+
init_vocabulary();
|
|
1024
|
+
MIN_STARS = 50;
|
|
1025
|
+
MIN_CONTRIBUTORS = 10;
|
|
1026
|
+
CANDIDATE_PR_PAGE = 50;
|
|
1027
|
+
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;
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
// ../../packages/core/src/matcher.ts
|
|
1032
|
+
function acceptanceDomainsOf(job) {
|
|
1033
|
+
return job.coreTags && job.coreTags.length > 0 ? job.coreTags : job.tags;
|
|
1034
|
+
}
|
|
1035
|
+
function backgroundIdf(tag) {
|
|
1036
|
+
const df = IDF_BACKGROUND.df[tag] ?? 0;
|
|
1037
|
+
return Math.log((IDF_BACKGROUND.N + 1) / (df + 1)) + 1;
|
|
1038
|
+
}
|
|
1039
|
+
function inferSeniority2(title) {
|
|
610
1040
|
if (!ENG_TITLE.test(title)) return void 0;
|
|
611
1041
|
for (const [re, level] of SENIORITY_PATTERNS) {
|
|
612
1042
|
if (re.test(title)) return level;
|
|
@@ -615,7 +1045,7 @@ function inferSeniority(title) {
|
|
|
615
1045
|
}
|
|
616
1046
|
function seniorityScore(fp, job) {
|
|
617
1047
|
if (!fp.seniorityBand) return 1;
|
|
618
|
-
const jobLevel =
|
|
1048
|
+
const jobLevel = inferSeniority2(job.title);
|
|
619
1049
|
if (!jobLevel) return 0.85;
|
|
620
1050
|
const wanted = SENIORITY_RANK[fp.seniorityBand] ?? 1;
|
|
621
1051
|
const got = SENIORITY_RANK[jobLevel] ?? 1;
|
|
@@ -625,8 +1055,10 @@ function seniorityScore(fp, job) {
|
|
|
625
1055
|
return 0.4;
|
|
626
1056
|
}
|
|
627
1057
|
function recencyScore(postedAt, now) {
|
|
628
|
-
if (!postedAt) return
|
|
629
|
-
const
|
|
1058
|
+
if (!postedAt) return UNKNOWN_RECENCY;
|
|
1059
|
+
const ms = new Date(postedAt).getTime();
|
|
1060
|
+
if (Number.isNaN(ms)) return UNKNOWN_RECENCY;
|
|
1061
|
+
const ageDays2 = (now - ms) / 864e5;
|
|
630
1062
|
if (ageDays2 < 7) return 1;
|
|
631
1063
|
if (ageDays2 < 30) return 0.9;
|
|
632
1064
|
if (ageDays2 < 90) return 0.75;
|
|
@@ -657,9 +1089,8 @@ function harmonicMean(a, b) {
|
|
|
657
1089
|
if (a <= 0 || b <= 0) return 0;
|
|
658
1090
|
return 2 * a * b / (a + b);
|
|
659
1091
|
}
|
|
660
|
-
function match(fp, jobs, limit = 5, now = Date.now()) {
|
|
661
|
-
const
|
|
662
|
-
const idfOf = (t) => idf.get(t) ?? 0;
|
|
1092
|
+
function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
|
|
1093
|
+
const idfOf = backgroundIdf;
|
|
663
1094
|
const expanded = expandWeighted(fp.skillTags);
|
|
664
1095
|
const maxDevScore = fp.skillTags.reduce((acc, t) => acc + idfOf(t), 0);
|
|
665
1096
|
const candidates = jobs.filter((j) => passesFilters(fp, j));
|
|
@@ -685,32 +1116,45 @@ function match(fp, jobs, limit = 5, now = Date.now()) {
|
|
|
685
1116
|
const jobCov = jobMaxScore > 0 ? Math.min(1, jobMatchScore / jobMaxScore) : 0;
|
|
686
1117
|
const tagComponent = harmonicMean(devCov, jobCov);
|
|
687
1118
|
if (tagComponent === 0) return null;
|
|
1119
|
+
const coreTags = job.coreTags ?? coreTagsFromTitle(job.title);
|
|
1120
|
+
let coreComponent = tagComponent;
|
|
1121
|
+
if (coreTags.length > 0) {
|
|
1122
|
+
const coreCov = Math.max(0, ...coreTags.map((ct) => expanded.get(ct)?.weight ?? 0));
|
|
1123
|
+
if (coreCov === 0) coreComponent = tagComponent * CORE_MISS_PENALTY;
|
|
1124
|
+
}
|
|
688
1125
|
details.sort((a, b) => idfOf(b.tag) * b.weight - idfOf(a.tag) * a.weight);
|
|
689
1126
|
const sScore = seniorityScore(fp, job);
|
|
690
1127
|
const rScore = recencyScore(job.postedAt, now);
|
|
691
|
-
const score =
|
|
1128
|
+
const score = coreComponent * 0.6 + sScore * 0.25 + rScore * 0.15;
|
|
692
1129
|
const matchedTags = [...new Set(details.map((d) => d.via ?? d.tag))];
|
|
1130
|
+
const badge = opts.acceptance ? bestAcceptanceDomain(opts.acceptance, acceptanceDomainsOf(job)) : null;
|
|
693
1131
|
return {
|
|
694
1132
|
job,
|
|
695
1133
|
score: Math.round(score * 1e3) / 1e3,
|
|
696
1134
|
matchedTags,
|
|
697
1135
|
matchDetails: details,
|
|
1136
|
+
...badge ? { acceptance: { status: "ok", domain: badge.domain, count: badge.count } } : {},
|
|
698
1137
|
reason: buildReason(details)
|
|
699
1138
|
};
|
|
700
1139
|
});
|
|
701
|
-
return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) =>
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1140
|
+
return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => {
|
|
1141
|
+
const byScore = b.score - a.score;
|
|
1142
|
+
if (Math.abs(byScore) > TIEBREAK_EPS) return byScore;
|
|
1143
|
+
const byAcceptance = (b.acceptance?.count ?? 0) - (a.acceptance?.count ?? 0);
|
|
1144
|
+
if (byAcceptance !== 0) return byAcceptance;
|
|
1145
|
+
return byScore;
|
|
1146
|
+
}).slice(0, limit);
|
|
1147
|
+
}
|
|
1148
|
+
var MIN_SCORE, TIEBREAK_EPS, SHARPEN, CORE_MISS_PENALTY, SENIORITY_RANK, SENIORITY_PATTERNS, ENG_TITLE, UNKNOWN_RECENCY;
|
|
708
1149
|
var init_matcher = __esm({
|
|
709
1150
|
"../../packages/core/src/matcher.ts"() {
|
|
710
1151
|
"use strict";
|
|
711
1152
|
init_vocabulary();
|
|
1153
|
+
init_github();
|
|
712
1154
|
MIN_SCORE = 0.15;
|
|
1155
|
+
TIEBREAK_EPS = 5e-3;
|
|
713
1156
|
SHARPEN = 1.6;
|
|
1157
|
+
CORE_MISS_PENALTY = 0.4;
|
|
714
1158
|
SENIORITY_RANK = {
|
|
715
1159
|
junior: 0,
|
|
716
1160
|
mid: 1,
|
|
@@ -724,24 +1168,19 @@ var init_matcher = __esm({
|
|
|
724
1168
|
[/\bmid[\s-]?level\b|\bmid\b/i, "mid"]
|
|
725
1169
|
];
|
|
726
1170
|
ENG_TITLE = /\b(engineer|engineering|developer|dev|swe|sde|programmer|architect)\b/i;
|
|
1171
|
+
UNKNOWN_RECENCY = 0.75;
|
|
727
1172
|
}
|
|
728
1173
|
});
|
|
729
1174
|
|
|
730
1175
|
// ../../packages/core/src/feeds/greenhouse.ts
|
|
731
|
-
function tokenize(text) {
|
|
732
|
-
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
733
|
-
}
|
|
734
1176
|
function extractTags(job) {
|
|
735
|
-
const
|
|
736
|
-
job.title,
|
|
1177
|
+
const body = [
|
|
737
1178
|
...(job.departments ?? []).map((d) => d.name),
|
|
738
1179
|
job.location?.name ?? "",
|
|
739
1180
|
...(job.offices ?? []).map((o) => o.name),
|
|
740
|
-
// mine the full HTML description for additional signal when present
|
|
741
1181
|
...job.content ? [job.content.replace(/<[^>]*>/g, " ")] : []
|
|
742
|
-
].filter(Boolean);
|
|
743
|
-
|
|
744
|
-
return normalize(tokens);
|
|
1182
|
+
].filter(Boolean).join(" ");
|
|
1183
|
+
return extractSkillTags(job.title, body);
|
|
745
1184
|
}
|
|
746
1185
|
function inferRemote(location) {
|
|
747
1186
|
const l = location.toLowerCase();
|
|
@@ -839,17 +1278,15 @@ var init_greenhouse = __esm({
|
|
|
839
1278
|
});
|
|
840
1279
|
|
|
841
1280
|
// ../../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
1281
|
function extractTags2(job) {
|
|
846
|
-
const
|
|
847
|
-
job.
|
|
848
|
-
job.
|
|
849
|
-
job.
|
|
850
|
-
...(job.secondaryLocations ?? []).map((l) => l.
|
|
851
|
-
|
|
852
|
-
|
|
1282
|
+
const body = [
|
|
1283
|
+
job.team ?? "",
|
|
1284
|
+
job.department ?? "",
|
|
1285
|
+
job.location ?? "",
|
|
1286
|
+
...(job.secondaryLocations ?? []).map((l) => l.location ?? ""),
|
|
1287
|
+
job.descriptionPlain ?? ""
|
|
1288
|
+
].join(" ");
|
|
1289
|
+
return extractSkillTags(job.title, body);
|
|
853
1290
|
}
|
|
854
1291
|
function mapEmploymentType(raw) {
|
|
855
1292
|
if (!raw) return "full_time";
|
|
@@ -860,7 +1297,7 @@ function mapEmploymentType(raw) {
|
|
|
860
1297
|
}
|
|
861
1298
|
function inferRemote2(job) {
|
|
862
1299
|
if (job.isRemote === true) return true;
|
|
863
|
-
const loc = (job.
|
|
1300
|
+
const loc = (job.location ?? "").toLowerCase();
|
|
864
1301
|
return loc.includes("remote") || loc.includes("anywhere");
|
|
865
1302
|
}
|
|
866
1303
|
async function fetchSlug2(slug) {
|
|
@@ -879,14 +1316,14 @@ async function fetchSlug2(slug) {
|
|
|
879
1316
|
source: "ashby",
|
|
880
1317
|
title: j.title,
|
|
881
1318
|
company: slug,
|
|
882
|
-
url: j.applyUrl ?? `https://jobs.ashbyhq.com/${slug}/${j.id}`,
|
|
1319
|
+
url: j.jobUrl ?? j.applyUrl ?? `https://jobs.ashbyhq.com/${slug}/${j.id}`,
|
|
883
1320
|
remote: inferRemote2(j),
|
|
884
|
-
location: j.
|
|
1321
|
+
location: j.location,
|
|
885
1322
|
compMin: comp?.minValue,
|
|
886
1323
|
compMax: comp?.maxValue,
|
|
887
1324
|
tags: extractTags2(j),
|
|
888
1325
|
roleType: mapEmploymentType(j.employmentType),
|
|
889
|
-
postedAt: j.
|
|
1326
|
+
postedAt: j.publishedAt,
|
|
890
1327
|
applyMode: "direct",
|
|
891
1328
|
raw: j
|
|
892
1329
|
};
|
|
@@ -913,20 +1350,16 @@ var init_ashby = __esm({
|
|
|
913
1350
|
});
|
|
914
1351
|
|
|
915
1352
|
// ../../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
1353
|
function extractTags3(p) {
|
|
920
1354
|
const cat = p.categories ?? {};
|
|
921
|
-
const
|
|
922
|
-
p.text,
|
|
1355
|
+
const body = [
|
|
923
1356
|
cat.team ?? "",
|
|
924
1357
|
cat.department ?? "",
|
|
925
1358
|
cat.location ?? "",
|
|
926
1359
|
...cat.allLocations ?? [],
|
|
927
1360
|
p.descriptionPlain ?? ""
|
|
928
|
-
];
|
|
929
|
-
return
|
|
1361
|
+
].join(" ");
|
|
1362
|
+
return extractSkillTags(p.text, body);
|
|
930
1363
|
}
|
|
931
1364
|
function mapCommitment(raw) {
|
|
932
1365
|
if (!raw) return "full_time";
|
|
@@ -999,15 +1432,8 @@ var init_lever = __esm({
|
|
|
999
1432
|
});
|
|
1000
1433
|
|
|
1001
1434
|
// ../../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
1435
|
function extractTags4(job) {
|
|
1006
|
-
|
|
1007
|
-
job.title,
|
|
1008
|
-
...job.tags ?? []
|
|
1009
|
-
];
|
|
1010
|
-
return normalize(texts.flatMap(tokenize4));
|
|
1436
|
+
return extractSkillTags(job.title, (job.tags ?? []).join(" "));
|
|
1011
1437
|
}
|
|
1012
1438
|
function mapJobType(raw) {
|
|
1013
1439
|
if (!raw) return "full_time";
|
|
@@ -1092,9 +1518,6 @@ var init_entities = __esm({
|
|
|
1092
1518
|
});
|
|
1093
1519
|
|
|
1094
1520
|
// ../../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
1521
|
function stripHtml(html) {
|
|
1099
1522
|
return html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
1100
1523
|
}
|
|
@@ -1138,8 +1561,8 @@ function parseRss(xml) {
|
|
|
1138
1561
|
return items;
|
|
1139
1562
|
}
|
|
1140
1563
|
function extractTags5(item) {
|
|
1141
|
-
const
|
|
1142
|
-
return
|
|
1564
|
+
const body = [item.category, stripHtml(item.description)].join(" ");
|
|
1565
|
+
return extractSkillTags(item.title, body);
|
|
1143
1566
|
}
|
|
1144
1567
|
var WWR_RSS_URL, wwr;
|
|
1145
1568
|
var init_wwr = __esm({
|
|
@@ -1181,9 +1604,6 @@ var init_wwr = __esm({
|
|
|
1181
1604
|
});
|
|
1182
1605
|
|
|
1183
1606
|
// ../../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
1607
|
function stripHtml2(html) {
|
|
1188
1608
|
return decodeEntities(html.replace(/<p>/gi, " ").replace(/<[^>]*>/g, "")).replace(/\s+/g, " ").trim();
|
|
1189
1609
|
}
|
|
@@ -1214,7 +1634,7 @@ function parseComment(item) {
|
|
|
1214
1634
|
return null;
|
|
1215
1635
|
}
|
|
1216
1636
|
const url = extractUrl(raw) || `https://news.ycombinator.com/item?id=${item.id}`;
|
|
1217
|
-
const tags = extractTags6(raw);
|
|
1637
|
+
const tags = extractTags6(title, raw);
|
|
1218
1638
|
if (tags.length === 0) return null;
|
|
1219
1639
|
return {
|
|
1220
1640
|
id: `hn:${item.id}`,
|
|
@@ -1231,8 +1651,8 @@ function parseComment(item) {
|
|
|
1231
1651
|
raw: item
|
|
1232
1652
|
};
|
|
1233
1653
|
}
|
|
1234
|
-
function extractTags6(text) {
|
|
1235
|
-
return
|
|
1654
|
+
function extractTags6(title, text) {
|
|
1655
|
+
return extractSkillTags(title, text);
|
|
1236
1656
|
}
|
|
1237
1657
|
var ALGOLIA_SEARCH, ALGOLIA_ITEMS, hn;
|
|
1238
1658
|
var init_hn = __esm({
|
|
@@ -1322,7 +1742,7 @@ function authHeaders() {
|
|
|
1322
1742
|
if (token) h["Authorization"] = `Bearer ${token}`;
|
|
1323
1743
|
return h;
|
|
1324
1744
|
}
|
|
1325
|
-
function
|
|
1745
|
+
function tokenize2(text) {
|
|
1326
1746
|
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
1327
1747
|
}
|
|
1328
1748
|
function parseAmountUSD(text) {
|
|
@@ -1407,7 +1827,7 @@ async function fetchRepoBounties(repoFullName) {
|
|
|
1407
1827
|
const body = issue.body ? decodeEntities(issue.body) : "";
|
|
1408
1828
|
const amountUSD = parseAmountUSD(title) ?? parseAmountUSD(body) ?? await fetchCommentAmount(repoFullName, issue.number);
|
|
1409
1829
|
const labels = labelNames(issue);
|
|
1410
|
-
const tags = normalize(
|
|
1830
|
+
const tags = normalize(tokenize2([title, labels.join(" "), body.slice(0, 2e3)].join(" ")));
|
|
1411
1831
|
return {
|
|
1412
1832
|
id: `bounty:${repoFullName}#${issue.number}`,
|
|
1413
1833
|
source: "bounty",
|
|
@@ -1724,103 +2144,6 @@ var init_indexer = __esm({
|
|
|
1724
2144
|
}
|
|
1725
2145
|
});
|
|
1726
2146
|
|
|
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
2147
|
// ../../packages/core/src/index.ts
|
|
1825
2148
|
var src_exports = {};
|
|
1826
2149
|
__export(src_exports, {
|
|
@@ -1834,17 +2157,23 @@ __export(src_exports, {
|
|
|
1834
2157
|
FEEDS: () => FEEDS,
|
|
1835
2158
|
GRAPH: () => GRAPH,
|
|
1836
2159
|
GREENHOUSE_SLUGS_BY_TIER: () => GREENHOUSE_SLUGS_BY_TIER,
|
|
2160
|
+
IDF_BACKGROUND: () => IDF_BACKGROUND,
|
|
1837
2161
|
LEVER_SLUGS_BY_TIER: () => LEVER_SLUGS_BY_TIER,
|
|
1838
2162
|
SYNONYMS: () => SYNONYMS,
|
|
1839
2163
|
VOCABULARY: () => VOCABULARY,
|
|
1840
2164
|
VOCAB_NODES: () => VOCAB_NODES,
|
|
2165
|
+
acceptanceCountForDomains: () => acceptanceCountForDomains,
|
|
1841
2166
|
aggregate: () => aggregate,
|
|
1842
2167
|
aggregateBounties: () => aggregateBounties,
|
|
1843
2168
|
ashby: () => ashby,
|
|
2169
|
+
bestAcceptanceDomain: () => bestAcceptanceDomain,
|
|
1844
2170
|
buildGraph: () => buildGraph,
|
|
1845
2171
|
buildIndex: () => buildIndex,
|
|
1846
2172
|
buildReason: () => buildReason,
|
|
2173
|
+
computeAcceptanceCredential: () => computeAcceptanceCredential,
|
|
2174
|
+
coreTagsFromTitle: () => coreTagsFromTitle,
|
|
1847
2175
|
expandWeighted: () => expandWeighted,
|
|
2176
|
+
extractSkillTags: () => extractSkillTags,
|
|
1848
2177
|
fetchGitHubProfile: () => fetchGitHubProfile,
|
|
1849
2178
|
flattenTiers: () => flattenTiers,
|
|
1850
2179
|
getBuyer: () => getBuyer,
|
|
@@ -1856,10 +2185,11 @@ __export(src_exports, {
|
|
|
1856
2185
|
isBounty: () => isBounty,
|
|
1857
2186
|
lever: () => lever,
|
|
1858
2187
|
loadPartnerRoles: () => loadPartnerRoles,
|
|
2188
|
+
looksLikeEngRole: () => looksLikeEngRole,
|
|
1859
2189
|
match: () => match,
|
|
1860
|
-
matchOne: () => matchOne,
|
|
1861
2190
|
normalize: () => normalize,
|
|
1862
2191
|
passesMaturityGate: () => passesMaturityGate,
|
|
2192
|
+
tokenize: () => tokenize,
|
|
1863
2193
|
validateGraph: () => validateGraph,
|
|
1864
2194
|
wwr: () => wwr
|
|
1865
2195
|
});
|
|
@@ -2131,7 +2461,7 @@ async function run() {
|
|
|
2131
2461
|
}
|
|
2132
2462
|
async function runLogin() {
|
|
2133
2463
|
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));
|
|
2464
|
+
const { fetchGitHubProfile: fetchGitHubProfile2, githubToFingerprint: githubToFingerprint2, computeAcceptanceCredential: computeAcceptanceCredential2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
2135
2465
|
const { readProfile: readProfile2, writeProfile: writeProfile2, accumulateGitHubTags: accumulateGitHubTags2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
|
|
2136
2466
|
console.log("");
|
|
2137
2467
|
console.log(" terminalhire \u2014 Sign in with GitHub");
|
|
@@ -2147,7 +2477,7 @@ async function runLogin() {
|
|
|
2147
2477
|
console.log(`
|
|
2148
2478
|
Fetching public profile for @${login}...`);
|
|
2149
2479
|
let ghProfile;
|
|
2150
|
-
if (process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["
|
|
2480
|
+
if (process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["JPI_GITHUB_MOCK"] === "1") {
|
|
2151
2481
|
const { createRequire: createRequire2 } = await import("module");
|
|
2152
2482
|
const { fileURLToPath: fileURLToPath7 } = await import("url");
|
|
2153
2483
|
const { join: join15, dirname: dirname3 } = await import("path");
|
|
@@ -2173,6 +2503,15 @@ async function runLogin() {
|
|
|
2173
2503
|
topLanguages: ghProfile.topLanguages.slice(0, 5),
|
|
2174
2504
|
publicRepos: ghProfile.publicRepos
|
|
2175
2505
|
};
|
|
2506
|
+
const isMock = process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["JPI_GITHUB_MOCK"] === "1";
|
|
2507
|
+
if (!isMock) {
|
|
2508
|
+
try {
|
|
2509
|
+
console.log(" Computing proof-of-work acceptance credential...");
|
|
2510
|
+
profile.acceptance = await computeAcceptanceCredential2(ghProfile.login, token);
|
|
2511
|
+
} catch (err) {
|
|
2512
|
+
if (process.env["DEBUG"]) console.warn(" [acceptance] credential compute failed:", err);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2176
2515
|
await writeProfile2(profile);
|
|
2177
2516
|
console.log("");
|
|
2178
2517
|
console.log(" GitHub profile merged into local encrypted profile:");
|
|
@@ -2189,6 +2528,9 @@ async function runLogin() {
|
|
|
2189
2528
|
if (fragment.skillTags.length === 0) {
|
|
2190
2529
|
console.log(" (No matching vocabulary tags found in public repos/topics)");
|
|
2191
2530
|
}
|
|
2531
|
+
if (profile.acceptance?.status === "ok" && profile.acceptance.qualifyingTotal > 0) {
|
|
2532
|
+
console.log(` Proof-of-work: ${profile.acceptance.qualifyingTotal} merged PR${profile.acceptance.qualifyingTotal === 1 ? "" : "s"} into external repos`);
|
|
2533
|
+
}
|
|
2192
2534
|
console.log("");
|
|
2193
2535
|
console.log(" Profile updated at ~/.terminalhire/profile.enc (encrypted at rest)");
|
|
2194
2536
|
console.log(" GitHub data stays on your machine unless you consent to share it in a lead.");
|
|
@@ -2210,10 +2552,18 @@ async function runLogout() {
|
|
|
2210
2552
|
}
|
|
2211
2553
|
await deleteGitHubToken2();
|
|
2212
2554
|
const profile = await readProfile2();
|
|
2555
|
+
let changed = false;
|
|
2213
2556
|
if (profile.github) {
|
|
2214
2557
|
delete profile.github;
|
|
2558
|
+
changed = true;
|
|
2559
|
+
}
|
|
2560
|
+
if (profile.acceptance) {
|
|
2561
|
+
delete profile.acceptance;
|
|
2562
|
+
changed = true;
|
|
2563
|
+
}
|
|
2564
|
+
if (changed) {
|
|
2215
2565
|
await writeProfile2(profile);
|
|
2216
|
-
console.log("\n GitHub identity cleared from local profile.");
|
|
2566
|
+
console.log("\n GitHub identity + proof-of-work credential cleared from local profile.");
|
|
2217
2567
|
}
|
|
2218
2568
|
console.log(" GitHub token deleted from ~/.terminalhire/github-token.enc");
|
|
2219
2569
|
console.log(" Skill tags accumulated from GitHub remain in your profile.");
|
|
@@ -2336,7 +2686,7 @@ function linkTitle(title, url) {
|
|
|
2336
2686
|
return url ? `${title} (${url})` : title;
|
|
2337
2687
|
}
|
|
2338
2688
|
function printResult(i, result) {
|
|
2339
|
-
const { job, score, matchedTags, reason } = result;
|
|
2689
|
+
const { job, score, matchedTags, reason, acceptance } = result;
|
|
2340
2690
|
const comp = formatComp(job);
|
|
2341
2691
|
const remote = job.remote ? "remote" : job.location ?? "onsite";
|
|
2342
2692
|
const compStr = comp ? ` \xB7 ${comp}` : "";
|
|
@@ -2348,6 +2698,9 @@ ${i + 1}. ${titleStr} \u2014 ${job.company}${mode}`);
|
|
|
2348
2698
|
console.log(` ${remote}${compStr} \xB7 ${job.roleType} \xB7 score: ${formatScore(score)}`);
|
|
2349
2699
|
console.log(` ${reason}`);
|
|
2350
2700
|
console.log(` Tags matched: ${matchedTags.slice(0, 5).join(", ")}`);
|
|
2701
|
+
if (acceptance && acceptance.status === "ok" && acceptance.count > 0) {
|
|
2702
|
+
console.log(` \u2713 proof-of-work: ${acceptance.count} merged PR${acceptance.count === 1 ? "" : "s"} into external ${acceptance.domain} repos`);
|
|
2703
|
+
}
|
|
2351
2704
|
if (job.applyMode === "direct") {
|
|
2352
2705
|
console.log(` Apply: ${job.url}`);
|
|
2353
2706
|
} else {
|
|
@@ -2405,7 +2758,9 @@ async function run2() {
|
|
|
2405
2758
|
}
|
|
2406
2759
|
const fp = profileToFingerprint2(profile);
|
|
2407
2760
|
if (REMOTE_ONLY) fp.prefs = { ...fp.prefs, remoteOnly: true };
|
|
2408
|
-
const results = match2(fp, jobs, SHOW_ALL ? jobs.length : LIMIT)
|
|
2761
|
+
const results = match2(fp, jobs, SHOW_ALL ? jobs.length : LIMIT, Date.now(), {
|
|
2762
|
+
acceptance: profile.acceptance
|
|
2763
|
+
});
|
|
2409
2764
|
try {
|
|
2410
2765
|
const cacheRaw = readFileSync4(INDEX_CACHE_FILE, "utf8");
|
|
2411
2766
|
const cacheEntry = JSON.parse(cacheRaw);
|