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.
@@ -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/matcher.ts
594
- function computeIdf(jobs) {
595
- const docFreq = /* @__PURE__ */ new Map();
596
- const N = jobs.length;
597
- for (const job of jobs) {
598
- const unique = new Set(job.tags);
599
- for (const tag of unique) {
600
- docFreq.set(tag, (docFreq.get(tag) ?? 0) + 1);
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 idf = /* @__PURE__ */ new Map();
604
- for (const [tag, df] of docFreq) {
605
- idf.set(tag, Math.log((N + 1) / (df + 1)) + 1);
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 idf;
1103
+ return scored.sort((a, b) => b.weight - a.weight).slice(0, 12).map((s) => s.t);
608
1104
  }
609
- function inferSeniority(title) {
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 = inferSeniority(job.title);
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 0.75;
629
- const ageDays2 = (now - new Date(postedAt).getTime()) / 864e5;
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 idf = computeIdf(jobs);
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 = tagComponent * 0.6 + sScore * 0.25 + rScore * 0.15;
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) => b.score - a.score).slice(0, limit);
702
- }
703
- function matchOne(fp, job) {
704
- const results = match(fp, [job], 1);
705
- return results.length > 0 ? results[0] : null;
706
- }
707
- var MIN_SCORE, SHARPEN, SENIORITY_RANK, SENIORITY_PATTERNS, ENG_TITLE;
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/greenhouse.ts
731
- function tokenize(text) {
732
- return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
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 texts = [
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
- const tokens = texts.flatMap(tokenize);
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 fetch(url, { headers: { Accept: "application/json" } });
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 texts = [
847
- job.title,
848
- job.teamName ?? "",
849
- job.locationName ?? "",
850
- ...(job.secondaryLocations ?? []).map((l) => l.locationName ?? "")
851
- ];
852
- return normalize(texts.flatMap(tokenize2));
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.locationName ?? "").toLowerCase();
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 fetch(url, {
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.locationName,
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.publishedDate,
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 texts = [
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 normalize(texts.flatMap(tokenize3));
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 fetch(url, { headers: { Accept: "application/json" } });
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
- const texts = [
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 fetch(url, {
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 text = [item.title, item.category, stripHtml(item.description)].join(" ");
1142
- return normalize(tokenize5(text));
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 fetch(WWR_RSS_URL, {
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: item.pubDate ? new Date(item.pubDate).toISOString() : void 0,
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 normalize(tokenize6(text));
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 fetch(ALGOLIA_SEARCH, {
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 fetch(`${ALGOLIA_ITEMS}${story.objectID}`, {
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 tokenize7(text) {
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 fetch(`${GITHUB_API}${path}`, { headers: authHeaders() });
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(tokenize7([title, labels.join(" "), body.slice(0, 2e3)].join(" ")));
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
- var GITHUB_API, BOUNTY_LABEL_RE, githubBounties;
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 repos = opts?.slugs && opts.slugs.length > 0 ? opts.slugs : DEFAULT_BOUNTY_REPOS;
1449
- console.info(`[github-bounties] scanning ${repos.length} repos`);
1450
- const settled = await Promise.allSettled(repos.map(fetchRepoBounties));
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 settled) {
1454
- if (r.status === "fulfilled") jobs.push(...r.value);
1455
- else {
2280
+ for (const r of results) {
2281
+ if (r.status === "fulfilled") {
2282
+ jobs.push(...r.value);
2283
+ } else {
1456
2284
  failures++;
1457
- console.warn("[github-bounties] repo fetch rejected:", r.reason);
2285
+ console.warn("[workable] account fetch rejected:", r.reason);
1458
2286
  }
1459
2287
  }
1460
- console.info(`[github-bounties] total: ${jobs.length} bounties, ${failures} repo failures`);
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
- return githubBounties.fetch({ slugs: opts?.repos });
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 githubBounties.fetch({ slugs: opts?.slugs?.["bounty"], limit });
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["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["JPI_GITHUB_MOCK"] === "1") {
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.unshift(`\u2726 Roles + \u{1F48E} paid bounties in your stack \u2014 link below`);
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;