terminalhire 0.3.3 → 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 +514 -181
- package/dist/bin/jpi-dispatch.js +561 -197
- package/dist/bin/jpi-jobs.js +521 -183
- package/dist/bin/jpi-learn.js +53 -10
- package/dist/bin/jpi-login.js +537 -184
- package/dist/bin/jpi-profile.js +53 -10
- package/dist/bin/jpi-refresh.js +531 -192
- package/dist/bin/jpi-save.js +53 -10
- package/dist/bin/jpi-spinner.js +5 -8
- package/dist/bin/jpi-sync.js +53 -10
- package/dist/bin/spinner.js +14 -10
- package/dist/src/profile.js +28 -2
- package/dist/src/signal.js +28 -2
- package/package.json +1 -1
package/dist/bin/jpi-login.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;
|
|
601
829
|
}
|
|
602
830
|
}
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
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;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
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
|
}
|
|
@@ -1119,10 +1542,13 @@ function parseRss(xml) {
|
|
|
1119
1542
|
return decodeEntities(plainMatch?.[1].trim() ?? "");
|
|
1120
1543
|
};
|
|
1121
1544
|
const rawTitle = get("title");
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
const titleAfterColon =
|
|
1545
|
+
const m = rawTitle.match(/^(.*?):\s+(.*)$/);
|
|
1546
|
+
let company = m ? m[1].trim() : "Unknown";
|
|
1547
|
+
const titleAfterColon = m ? m[2].trim() : rawTitle;
|
|
1125
1548
|
const title = titleAfterColon.replace(/\s*\([^)]*\)\s*$/, "").trim();
|
|
1549
|
+
if (/^https?:\/\//i.test(company)) {
|
|
1550
|
+
company = company.replace(/^https?:\/\//i, "").replace(/\/.*$/, "").trim() || "Unknown";
|
|
1551
|
+
}
|
|
1126
1552
|
items.push({
|
|
1127
1553
|
title,
|
|
1128
1554
|
link: get("link") || get("guid"),
|
|
@@ -1135,8 +1561,8 @@ function parseRss(xml) {
|
|
|
1135
1561
|
return items;
|
|
1136
1562
|
}
|
|
1137
1563
|
function extractTags5(item) {
|
|
1138
|
-
const
|
|
1139
|
-
return
|
|
1564
|
+
const body = [item.category, stripHtml(item.description)].join(" ");
|
|
1565
|
+
return extractSkillTags(item.title, body);
|
|
1140
1566
|
}
|
|
1141
1567
|
var WWR_RSS_URL, wwr;
|
|
1142
1568
|
var init_wwr = __esm({
|
|
@@ -1178,9 +1604,6 @@ var init_wwr = __esm({
|
|
|
1178
1604
|
});
|
|
1179
1605
|
|
|
1180
1606
|
// ../../packages/core/src/feeds/hn.ts
|
|
1181
|
-
function tokenize6(text) {
|
|
1182
|
-
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
1183
|
-
}
|
|
1184
1607
|
function stripHtml2(html) {
|
|
1185
1608
|
return decodeEntities(html.replace(/<p>/gi, " ").replace(/<[^>]*>/g, "")).replace(/\s+/g, " ").trim();
|
|
1186
1609
|
}
|
|
@@ -1211,7 +1634,7 @@ function parseComment(item) {
|
|
|
1211
1634
|
return null;
|
|
1212
1635
|
}
|
|
1213
1636
|
const url = extractUrl(raw) || `https://news.ycombinator.com/item?id=${item.id}`;
|
|
1214
|
-
const tags = extractTags6(raw);
|
|
1637
|
+
const tags = extractTags6(title, raw);
|
|
1215
1638
|
if (tags.length === 0) return null;
|
|
1216
1639
|
return {
|
|
1217
1640
|
id: `hn:${item.id}`,
|
|
@@ -1228,8 +1651,8 @@ function parseComment(item) {
|
|
|
1228
1651
|
raw: item
|
|
1229
1652
|
};
|
|
1230
1653
|
}
|
|
1231
|
-
function extractTags6(text) {
|
|
1232
|
-
return
|
|
1654
|
+
function extractTags6(title, text) {
|
|
1655
|
+
return extractSkillTags(title, text);
|
|
1233
1656
|
}
|
|
1234
1657
|
var ALGOLIA_SEARCH, ALGOLIA_ITEMS, hn;
|
|
1235
1658
|
var init_hn = __esm({
|
|
@@ -1319,7 +1742,7 @@ function authHeaders() {
|
|
|
1319
1742
|
if (token) h["Authorization"] = `Bearer ${token}`;
|
|
1320
1743
|
return h;
|
|
1321
1744
|
}
|
|
1322
|
-
function
|
|
1745
|
+
function tokenize2(text) {
|
|
1323
1746
|
return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
|
|
1324
1747
|
}
|
|
1325
1748
|
function parseAmountUSD(text) {
|
|
@@ -1404,7 +1827,7 @@ async function fetchRepoBounties(repoFullName) {
|
|
|
1404
1827
|
const body = issue.body ? decodeEntities(issue.body) : "";
|
|
1405
1828
|
const amountUSD = parseAmountUSD(title) ?? parseAmountUSD(body) ?? await fetchCommentAmount(repoFullName, issue.number);
|
|
1406
1829
|
const labels = labelNames(issue);
|
|
1407
|
-
const tags = normalize(
|
|
1830
|
+
const tags = normalize(tokenize2([title, labels.join(" "), body.slice(0, 2e3)].join(" ")));
|
|
1408
1831
|
return {
|
|
1409
1832
|
id: `bounty:${repoFullName}#${issue.number}`,
|
|
1410
1833
|
source: "bounty",
|
|
@@ -1721,103 +2144,6 @@ var init_indexer = __esm({
|
|
|
1721
2144
|
}
|
|
1722
2145
|
});
|
|
1723
2146
|
|
|
1724
|
-
// ../../packages/core/src/github.ts
|
|
1725
|
-
function ghHeaders(token) {
|
|
1726
|
-
const headers = {
|
|
1727
|
-
Accept: "application/vnd.github+json",
|
|
1728
|
-
"X-GitHub-Api-Version": "2022-11-28"
|
|
1729
|
-
};
|
|
1730
|
-
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
1731
|
-
return headers;
|
|
1732
|
-
}
|
|
1733
|
-
async function ghFetch(path, token) {
|
|
1734
|
-
const url = `https://api.github.com${path}`;
|
|
1735
|
-
const res = await fetch(url, { headers: ghHeaders(token) });
|
|
1736
|
-
if (!res.ok) {
|
|
1737
|
-
throw new Error(`GitHub API ${path}: HTTP ${res.status} ${res.statusText}`);
|
|
1738
|
-
}
|
|
1739
|
-
return res.json();
|
|
1740
|
-
}
|
|
1741
|
-
async function fetchGitHubProfile(login, token) {
|
|
1742
|
-
const user = await ghFetch(`/users/${login}`, token);
|
|
1743
|
-
let repos = [];
|
|
1744
|
-
try {
|
|
1745
|
-
repos = await ghFetch(
|
|
1746
|
-
`/users/${login}/repos?sort=pushed&per_page=100`,
|
|
1747
|
-
token
|
|
1748
|
-
);
|
|
1749
|
-
} catch (err) {
|
|
1750
|
-
console.warn(`[github] ${login}: repos fetch failed, continuing \u2014`, err);
|
|
1751
|
-
}
|
|
1752
|
-
const langCount = {};
|
|
1753
|
-
for (const repo of repos) {
|
|
1754
|
-
if (repo.fork) continue;
|
|
1755
|
-
if (repo.language) {
|
|
1756
|
-
langCount[repo.language.toLowerCase()] = (langCount[repo.language.toLowerCase()] ?? 0) + 1;
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
const topLanguages = Object.entries(langCount).sort(([, a], [, b]) => b - a).slice(0, 10).map(([lang]) => lang);
|
|
1760
|
-
const topicSet = /* @__PURE__ */ new Set();
|
|
1761
|
-
for (const repo of repos) {
|
|
1762
|
-
if (repo.fork) continue;
|
|
1763
|
-
for (const t of repo.topics ?? []) topicSet.add(t.toLowerCase());
|
|
1764
|
-
}
|
|
1765
|
-
const topics = Array.from(topicSet).slice(0, 30);
|
|
1766
|
-
let recentPRorgs;
|
|
1767
|
-
try {
|
|
1768
|
-
const q = encodeURIComponent(
|
|
1769
|
-
`type:pr is:merged author:${login} sort:updated`
|
|
1770
|
-
);
|
|
1771
|
-
const result = await ghFetch(
|
|
1772
|
-
`/search/issues?q=${q}&per_page=30`,
|
|
1773
|
-
token
|
|
1774
|
-
);
|
|
1775
|
-
const orgs = /* @__PURE__ */ new Set();
|
|
1776
|
-
for (const item of result.items ?? []) {
|
|
1777
|
-
const orgLogin = item.repository?.owner?.login;
|
|
1778
|
-
if (orgLogin && orgLogin !== login) orgs.add(orgLogin);
|
|
1779
|
-
}
|
|
1780
|
-
if (orgs.size > 0) recentPRorgs = Array.from(orgs);
|
|
1781
|
-
} catch {
|
|
1782
|
-
}
|
|
1783
|
-
return {
|
|
1784
|
-
login: user.login,
|
|
1785
|
-
name: user.name ?? void 0,
|
|
1786
|
-
publicEmail: user.email ?? void 0,
|
|
1787
|
-
avatarUrl: user.avatar_url,
|
|
1788
|
-
accountCreatedAt: user.created_at,
|
|
1789
|
-
publicRepos: user.public_repos,
|
|
1790
|
-
followers: user.followers,
|
|
1791
|
-
topLanguages,
|
|
1792
|
-
topics,
|
|
1793
|
-
recentPRorgs
|
|
1794
|
-
};
|
|
1795
|
-
}
|
|
1796
|
-
function inferSeniority2(p) {
|
|
1797
|
-
const ageMs = Date.now() - new Date(p.accountCreatedAt).getTime();
|
|
1798
|
-
const ageYears = ageMs / (1e3 * 60 * 60 * 24 * 365.25);
|
|
1799
|
-
if (ageYears >= 9 && (p.publicRepos >= 40 || p.followers >= 500)) return "staff";
|
|
1800
|
-
if (ageYears >= 5 && (p.publicRepos >= 20 || p.followers >= 100)) return "senior";
|
|
1801
|
-
if (ageYears >= 2 && p.publicRepos >= 5) return "mid";
|
|
1802
|
-
return "junior";
|
|
1803
|
-
}
|
|
1804
|
-
function githubToFingerprint(p) {
|
|
1805
|
-
const rawTokens = [
|
|
1806
|
-
...p.topLanguages,
|
|
1807
|
-
...p.topics
|
|
1808
|
-
// recentPRorgs intentionally excluded — org names are not skill tags
|
|
1809
|
-
];
|
|
1810
|
-
const skillTags = normalize(rawTokens);
|
|
1811
|
-
const seniorityBand = inferSeniority2(p);
|
|
1812
|
-
return { skillTags, seniorityBand };
|
|
1813
|
-
}
|
|
1814
|
-
var init_github = __esm({
|
|
1815
|
-
"../../packages/core/src/github.ts"() {
|
|
1816
|
-
"use strict";
|
|
1817
|
-
init_vocabulary();
|
|
1818
|
-
}
|
|
1819
|
-
});
|
|
1820
|
-
|
|
1821
2147
|
// ../../packages/core/src/index.ts
|
|
1822
2148
|
var src_exports = {};
|
|
1823
2149
|
__export(src_exports, {
|
|
@@ -1831,17 +2157,23 @@ __export(src_exports, {
|
|
|
1831
2157
|
FEEDS: () => FEEDS,
|
|
1832
2158
|
GRAPH: () => GRAPH,
|
|
1833
2159
|
GREENHOUSE_SLUGS_BY_TIER: () => GREENHOUSE_SLUGS_BY_TIER,
|
|
2160
|
+
IDF_BACKGROUND: () => IDF_BACKGROUND,
|
|
1834
2161
|
LEVER_SLUGS_BY_TIER: () => LEVER_SLUGS_BY_TIER,
|
|
1835
2162
|
SYNONYMS: () => SYNONYMS,
|
|
1836
2163
|
VOCABULARY: () => VOCABULARY,
|
|
1837
2164
|
VOCAB_NODES: () => VOCAB_NODES,
|
|
2165
|
+
acceptanceCountForDomains: () => acceptanceCountForDomains,
|
|
1838
2166
|
aggregate: () => aggregate,
|
|
1839
2167
|
aggregateBounties: () => aggregateBounties,
|
|
1840
2168
|
ashby: () => ashby,
|
|
2169
|
+
bestAcceptanceDomain: () => bestAcceptanceDomain,
|
|
1841
2170
|
buildGraph: () => buildGraph,
|
|
1842
2171
|
buildIndex: () => buildIndex,
|
|
1843
2172
|
buildReason: () => buildReason,
|
|
2173
|
+
computeAcceptanceCredential: () => computeAcceptanceCredential,
|
|
2174
|
+
coreTagsFromTitle: () => coreTagsFromTitle,
|
|
1844
2175
|
expandWeighted: () => expandWeighted,
|
|
2176
|
+
extractSkillTags: () => extractSkillTags,
|
|
1845
2177
|
fetchGitHubProfile: () => fetchGitHubProfile,
|
|
1846
2178
|
flattenTiers: () => flattenTiers,
|
|
1847
2179
|
getBuyer: () => getBuyer,
|
|
@@ -1853,10 +2185,11 @@ __export(src_exports, {
|
|
|
1853
2185
|
isBounty: () => isBounty,
|
|
1854
2186
|
lever: () => lever,
|
|
1855
2187
|
loadPartnerRoles: () => loadPartnerRoles,
|
|
2188
|
+
looksLikeEngRole: () => looksLikeEngRole,
|
|
1856
2189
|
match: () => match,
|
|
1857
|
-
matchOne: () => matchOne,
|
|
1858
2190
|
normalize: () => normalize,
|
|
1859
2191
|
passesMaturityGate: () => passesMaturityGate,
|
|
2192
|
+
tokenize: () => tokenize,
|
|
1860
2193
|
validateGraph: () => validateGraph,
|
|
1861
2194
|
wwr: () => wwr
|
|
1862
2195
|
});
|
|
@@ -2124,7 +2457,7 @@ async function run() {
|
|
|
2124
2457
|
}
|
|
2125
2458
|
async function runLogin() {
|
|
2126
2459
|
const { runDeviceFlow: runDeviceFlow2, readGitHubToken: readGitHubToken2 } = await Promise.resolve().then(() => (init_github_auth(), github_auth_exports));
|
|
2127
|
-
const { fetchGitHubProfile: fetchGitHubProfile2, githubToFingerprint: githubToFingerprint2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
2460
|
+
const { fetchGitHubProfile: fetchGitHubProfile2, githubToFingerprint: githubToFingerprint2, computeAcceptanceCredential: computeAcceptanceCredential2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
2128
2461
|
const { readProfile: readProfile2, writeProfile: writeProfile2, accumulateGitHubTags: accumulateGitHubTags2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
|
|
2129
2462
|
console.log("");
|
|
2130
2463
|
console.log(" terminalhire \u2014 Sign in with GitHub");
|
|
@@ -2140,7 +2473,7 @@ async function runLogin() {
|
|
|
2140
2473
|
console.log(`
|
|
2141
2474
|
Fetching public profile for @${login}...`);
|
|
2142
2475
|
let ghProfile;
|
|
2143
|
-
if (process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["
|
|
2476
|
+
if (process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["JPI_GITHUB_MOCK"] === "1") {
|
|
2144
2477
|
const { createRequire } = await import("module");
|
|
2145
2478
|
const { fileURLToPath: fileURLToPath2 } = await import("url");
|
|
2146
2479
|
const { join: join4, dirname } = await import("path");
|
|
@@ -2166,6 +2499,15 @@ async function runLogin() {
|
|
|
2166
2499
|
topLanguages: ghProfile.topLanguages.slice(0, 5),
|
|
2167
2500
|
publicRepos: ghProfile.publicRepos
|
|
2168
2501
|
};
|
|
2502
|
+
const isMock = process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["JPI_GITHUB_MOCK"] === "1";
|
|
2503
|
+
if (!isMock) {
|
|
2504
|
+
try {
|
|
2505
|
+
console.log(" Computing proof-of-work acceptance credential...");
|
|
2506
|
+
profile.acceptance = await computeAcceptanceCredential2(ghProfile.login, token);
|
|
2507
|
+
} catch (err) {
|
|
2508
|
+
if (process.env["DEBUG"]) console.warn(" [acceptance] credential compute failed:", err);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2169
2511
|
await writeProfile2(profile);
|
|
2170
2512
|
console.log("");
|
|
2171
2513
|
console.log(" GitHub profile merged into local encrypted profile:");
|
|
@@ -2182,6 +2524,9 @@ async function runLogin() {
|
|
|
2182
2524
|
if (fragment.skillTags.length === 0) {
|
|
2183
2525
|
console.log(" (No matching vocabulary tags found in public repos/topics)");
|
|
2184
2526
|
}
|
|
2527
|
+
if (profile.acceptance?.status === "ok" && profile.acceptance.qualifyingTotal > 0) {
|
|
2528
|
+
console.log(` Proof-of-work: ${profile.acceptance.qualifyingTotal} merged PR${profile.acceptance.qualifyingTotal === 1 ? "" : "s"} into external repos`);
|
|
2529
|
+
}
|
|
2185
2530
|
console.log("");
|
|
2186
2531
|
console.log(" Profile updated at ~/.terminalhire/profile.enc (encrypted at rest)");
|
|
2187
2532
|
console.log(" GitHub data stays on your machine unless you consent to share it in a lead.");
|
|
@@ -2203,10 +2548,18 @@ async function runLogout() {
|
|
|
2203
2548
|
}
|
|
2204
2549
|
await deleteGitHubToken2();
|
|
2205
2550
|
const profile = await readProfile2();
|
|
2551
|
+
let changed = false;
|
|
2206
2552
|
if (profile.github) {
|
|
2207
2553
|
delete profile.github;
|
|
2554
|
+
changed = true;
|
|
2555
|
+
}
|
|
2556
|
+
if (profile.acceptance) {
|
|
2557
|
+
delete profile.acceptance;
|
|
2558
|
+
changed = true;
|
|
2559
|
+
}
|
|
2560
|
+
if (changed) {
|
|
2208
2561
|
await writeProfile2(profile);
|
|
2209
|
-
console.log("\n GitHub identity cleared from local profile.");
|
|
2562
|
+
console.log("\n GitHub identity + proof-of-work credential cleared from local profile.");
|
|
2210
2563
|
}
|
|
2211
2564
|
console.log(" GitHub token deleted from ~/.terminalhire/github-token.enc");
|
|
2212
2565
|
console.log(" Skill tags accumulated from GitHub remain in your profile.");
|