terminalhire 0.18.0 → 0.18.2

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.
@@ -391,6 +391,11 @@ ${body}`;
391
391
  for (const tok of tokens) {
392
392
  const r = resolveToken(tok);
393
393
  if (!r) continue;
394
+ const synGate = AMBIGUOUS_SYNONYM[tok] ?? AMBIGUOUS_SYNONYM[tok.replace(/^[.\-+#]+|[.\-+#]+$/g, "")];
395
+ if (synGate && r.id === synGate.id) {
396
+ if (synGate.cue.test(text)) ids.add(r.id);
397
+ continue;
398
+ }
394
399
  if (NON_EXTRACTABLE.has(r.id)) continue;
395
400
  if (SYNONYM_ONLY.has(r.id) && !r.viaSynonym) continue;
396
401
  const cue = AMBIGUOUS[r.id];
@@ -408,7 +413,7 @@ ${body}`;
408
413
  function coreTagsFromTitle(title) {
409
414
  return extractSkillTags(title, "").filter((t) => !SOFT_DOMAIN.has(t));
410
415
  }
411
- var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, ENG_INTENT, NON_ENG_TITLE;
416
+ var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, AMBIGUOUS_SYNONYM, ENG_INTENT, NON_ENG_TITLE;
412
417
  var init_extract = __esm({
413
418
  "../../packages/core/src/vocab/extract.ts"() {
414
419
  "use strict";
@@ -445,6 +450,16 @@ var init_extract = __esm({
445
450
  r: /\b(rstudio|tidyverse|ggplot|shiny|dplyr|cran|r-lang|rlang)\b/i,
446
451
  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
447
452
  };
453
+ AMBIGUOUS_SYNONYM = {
454
+ // "prompt" is the LLM skill only in AI context. In raw JD prose it is "prompt
455
+ // delivery / response / payment / communication / attention". Accept only with
456
+ // the explicit "prompt engineering" phrase OR an LLM/AI ecosystem cue; the cue
457
+ // deliberately excludes the bare word "prompt(s)" itself so it can't self-satisfy.
458
+ prompt: {
459
+ id: "prompt-engineering",
460
+ cue: /\bprompt[\s-]?engineer(?:ing|s)?\b|\b(llms?|gpt-?[0-9o]*|claude|gemini|llama|mistral|openai|anthropic|langchain|llama[\s-]?index|rag|retrieval[\s-]?augmented|embeddings?|fine[\s-]?tun(?:e|ed|ing)|vector[\s-]?(?:db|database|store)|agentic|ai agents?|chatbots?|generative ai|gen[\s-]?ai|genai|few[\s-]?shot|zero[\s-]?shot)\b/i
461
+ }
462
+ };
448
463
  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;
449
464
  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;
450
465
  }
@@ -921,6 +936,7 @@ async function fetchRepoMeta(owner, name, token, cache) {
921
936
  stars: r.stargazers_count ?? 0,
922
937
  archived: !!r.archived,
923
938
  fork: !!r.fork,
939
+ private: !!r.private,
924
940
  language: r.language ?? null,
925
941
  topics: r.topics ?? [],
926
942
  contributors
@@ -953,7 +969,7 @@ async function computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates
953
969
  const loginLc = login.toLowerCase();
954
970
  let items;
955
971
  try {
956
- const q = encodeURIComponent(`type:pr is:merged author:${login} -user:${login} sort:updated`);
972
+ const q = encodeURIComponent(`type:pr is:merged is:public author:${login} -user:${login} sort:updated`);
957
973
  const res = await ghFetch(
958
974
  `/search/issues?q=${q}&per_page=${CANDIDATE_PR_PAGE}`,
959
975
  token
@@ -966,6 +982,7 @@ async function computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates
966
982
  }
967
983
  const byDomain = {};
968
984
  let qualifyingTotal = 0;
985
+ const qualifyingPRs = [];
969
986
  for (const item of items) {
970
987
  const repo = parseRepoUrl(item.repository_url);
971
988
  if (!repo) continue;
@@ -975,13 +992,22 @@ async function computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates
975
992
  if (isTrivialPRTitle(item.title)) continue;
976
993
  const meta = await fetchRepoMeta(repo.owner, repo.name, token, cache);
977
994
  if (!meta) continue;
995
+ if (meta.private) continue;
978
996
  if (meta.archived || meta.fork) continue;
979
997
  if (meta.stars < gates.minStars) continue;
980
998
  if (meta.contributors !== void 0 && meta.contributors < gates.minContributors) continue;
981
999
  qualifyingTotal += 1;
982
1000
  const mergedAt = item.pull_request?.merged_at ?? item.closed_at ?? item.created_at;
983
1001
  const rawDomains = [meta.language ?? "", ...meta.topics].filter(Boolean);
984
- for (const d of new Set(normalize(rawDomains))) {
1002
+ const domainTags = [...new Set(normalize(rawDomains))];
1003
+ qualifyingPRs.push({
1004
+ url: item.html_url,
1005
+ title: item.title,
1006
+ repo: `${repo.owner}/${repo.name}`,
1007
+ domains: domainTags,
1008
+ mergedAt
1009
+ });
1010
+ for (const d of domainTags) {
985
1011
  const b = byDomain[d] ?? (byDomain[d] = { mergedPRs: 0, distinctOrgs: 0, lastMergedAt: mergedAt, orgs: /* @__PURE__ */ new Set() });
986
1012
  b.mergedPRs += 1;
987
1013
  b.orgs.add(ownerLc);
@@ -996,7 +1022,7 @@ async function computeAcceptanceFromSearch(login, token, ownedOrgs, cache, gates
996
1022
  lastMergedAt: b.lastMergedAt
997
1023
  };
998
1024
  }
999
- return { status: "ok", byDomain: finalDomains, qualifyingTotal, computedAt };
1025
+ return { status: "ok", byDomain: finalDomains, qualifyingTotal, qualifyingPRs, computedAt };
1000
1026
  }
1001
1027
  async function computeAcceptanceCredential(login, token, cache = /* @__PURE__ */ new Map()) {
1002
1028
  if (!token) return emptyCredential("no-token");
@@ -1807,6 +1833,19 @@ var init_concurrency = __esm({
1807
1833
  }
1808
1834
  });
1809
1835
 
1836
+ // ../../packages/core/src/feeds/effort.ts
1837
+ function effortFromAmount(amount) {
1838
+ if (amount == null) return void 0;
1839
+ if (amount <= 500) return "small";
1840
+ if (amount <= 2e3) return "medium";
1841
+ return "large";
1842
+ }
1843
+ var init_effort = __esm({
1844
+ "../../packages/core/src/feeds/effort.ts"() {
1845
+ "use strict";
1846
+ }
1847
+ });
1848
+
1810
1849
  // ../../packages/core/src/feeds/github-bounties.ts
1811
1850
  function authHeaders() {
1812
1851
  const token = process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"];
@@ -1829,12 +1868,6 @@ function parseAmountUSD(text) {
1829
1868
  if (!Number.isFinite(n) || n <= 0 || n > 1e6) return void 0;
1830
1869
  return Math.round(n);
1831
1870
  }
1832
- function effortFromAmount(amount) {
1833
- if (amount == null) return void 0;
1834
- if (amount <= 500) return "small";
1835
- if (amount <= 2e3) return "medium";
1836
- return "large";
1837
- }
1838
1871
  function labelNames(issue) {
1839
1872
  return (issue.labels ?? []).map((l) => typeof l === "string" ? l : l.name ?? "").filter(Boolean);
1840
1873
  }
@@ -2082,6 +2115,7 @@ var init_github_bounties = __esm({
2082
2115
  init_bounty_gate();
2083
2116
  init_http();
2084
2117
  init_concurrency();
2118
+ init_effort();
2085
2119
  GITHUB_API = "https://api.github.com";
2086
2120
  BOUNTY_LABEL_RE = /bounty|reward|funded|💎|💰/i;
2087
2121
  SEARCH_QUERIES = [
@@ -2133,12 +2167,6 @@ var init_github_bounties = __esm({
2133
2167
  function tokenize3(text) {
2134
2168
  return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter((w) => w.length > 1);
2135
2169
  }
2136
- function effortFromAmount2(usd) {
2137
- if (usd == null) return void 0;
2138
- if (usd < 150) return "small";
2139
- if (usd < 750) return "medium";
2140
- return "large";
2141
- }
2142
2170
  function priceToUSD(p) {
2143
2171
  if (!p || typeof p.value !== "number") return void 0;
2144
2172
  if (p.unit === "USD_CENT") return Math.round(p.value) / 100;
@@ -2161,6 +2189,7 @@ var init_opire = __esm({
2161
2189
  init_bounty_gate();
2162
2190
  init_github_bounties();
2163
2191
  init_http();
2192
+ init_effort();
2164
2193
  OPIRE_REWARDS_URL = "https://api.opire.dev/rewards";
2165
2194
  MIN_USD = 25;
2166
2195
  MAX_USD = 25e3;
@@ -2197,7 +2226,7 @@ var init_opire = __esm({
2197
2226
  const tags = normalize([...r.programmingLanguages ?? [], ...tokenize3(title)]);
2198
2227
  const bounty = {
2199
2228
  amountUSD,
2200
- estimatedEffort: effortFromAmount2(amountUSD),
2229
+ estimatedEffort: effortFromAmount(amountUSD),
2201
2230
  bountySource: "opire",
2202
2231
  claimUrl: r.url,
2203
2232
  repoFullName
@@ -6544,6 +6573,7 @@ __export(src_exports, {
6544
6573
  isBounty: () => isBounty,
6545
6574
  isContribution: () => isContribution,
6546
6575
  isOverIntroLimit: () => isOverIntroLimit,
6576
+ isTrivialPRTitle: () => isTrivialPRTitle,
6547
6577
  joinLabels: () => joinLabels,
6548
6578
  labelFor: () => labelFor,
6549
6579
  lever: () => lever,
@@ -6600,9 +6630,9 @@ var init_keytar = __esm({
6600
6630
  }
6601
6631
  });
6602
6632
 
6603
- // node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/rel-0180/node_modules/keytar/build/Release/keytar.node
6633
+ // node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node
6604
6634
  var require_keytar = __commonJS({
6605
- "node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/rel-0180/node_modules/keytar/build/Release/keytar.node"(exports, module) {
6635
+ "node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node"(exports, module) {
6606
6636
  "use strict";
6607
6637
  init_keytar();
6608
6638
  try {
@@ -6974,6 +7004,37 @@ function excludeOwnCard(results, ownLogin) {
6974
7004
  });
6975
7005
  }
6976
7006
 
7007
+ // bin/sanitize.js
7008
+ var CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
7009
+ function sanitizeText(s) {
7010
+ if (s == null) return "";
7011
+ return String(s).replace(CONTROL_CHARS, "");
7012
+ }
7013
+ function safeHttpUrl(url) {
7014
+ if (url == null) return null;
7015
+ const raw = String(url);
7016
+ CONTROL_CHARS.lastIndex = 0;
7017
+ if (CONTROL_CHARS.test(raw)) return null;
7018
+ let parsed;
7019
+ try {
7020
+ parsed = new URL(raw);
7021
+ } catch {
7022
+ return null;
7023
+ }
7024
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
7025
+ return parsed.href;
7026
+ }
7027
+ function linkTitle(title, url) {
7028
+ const safeTitle = sanitizeText(title);
7029
+ const href = safeHttpUrl(url);
7030
+ const isTTY = process.stdout.isTTY;
7031
+ const noColor = process.env["NO_COLOR"] !== void 0;
7032
+ if (isTTY && !noColor && href) {
7033
+ return `\x1B]8;;${href}\x1B\\${safeTitle}\x1B]8;;\x1B\\`;
7034
+ }
7035
+ return href ? `${safeTitle} (${href})` : safeTitle;
7036
+ }
7037
+
6977
7038
  // bin/jpi-devs.js
6978
7039
  var API_URL2 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
6979
7040
  var DEFAULT_LIMIT = 10;
@@ -6995,24 +7056,18 @@ function credentialUrl(job) {
6995
7056
  const u = job.url ?? "";
6996
7057
  return u.startsWith("http") ? u : `${API_URL2}${u}`;
6997
7058
  }
6998
- function linkTitle(title, url) {
6999
- const isTTY = process.stdout.isTTY;
7000
- const noColor = process.env["NO_COLOR"] !== void 0;
7001
- if (isTTY && !noColor && url) return `\x1B]8;;${url}\x1B\\${title}\x1B]8;;\x1B\\`;
7002
- return url ? `${title} (${url})` : title;
7003
- }
7004
7059
  function printCard(i, result) {
7005
7060
  const { job, score, matchedTags, reason } = result;
7006
7061
  const url = credentialUrl(job);
7007
7062
  const kind = job.source === "project" ? "project" : "developer";
7008
- const byline = job.source === "project" ? ` \xB7 by @${job.company}` : "";
7063
+ const byline = job.source === "project" ? ` \xB7 by @${sanitizeText(job.company)}` : "";
7009
7064
  const scoreStr = score > 0 ? ` \xB7 match ${Math.round(score * 100)}%` : "";
7010
7065
  console.log(`
7011
7066
  ${i + 1}. ${linkTitle(job.title, url)} \u2014 ${kind}${byline}${scoreStr}`);
7012
7067
  console.log(` id: ${job.id}`);
7013
7068
  if (reason) console.log(` ${reason}`);
7014
7069
  if (matchedTags && matchedTags.length) console.log(` Tags matched: ${matchedTags.slice(0, 5).join(", ")}`);
7015
- console.log(` Profile: ${url}`);
7070
+ console.log(` Profile: ${sanitizeText(url)}`);
7016
7071
  }
7017
7072
  async function run() {
7018
7073
  try {