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 {
@@ -6928,6 +6958,37 @@ function updateIndexCache(patch) {
6928
6958
  return entry;
6929
6959
  }
6930
6960
 
6961
+ // bin/sanitize.js
6962
+ var CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
6963
+ function sanitizeText(s) {
6964
+ if (s == null) return "";
6965
+ return String(s).replace(CONTROL_CHARS, "");
6966
+ }
6967
+ function safeHttpUrl(url) {
6968
+ if (url == null) return null;
6969
+ const raw = String(url);
6970
+ CONTROL_CHARS.lastIndex = 0;
6971
+ if (CONTROL_CHARS.test(raw)) return null;
6972
+ let parsed;
6973
+ try {
6974
+ parsed = new URL(raw);
6975
+ } catch {
6976
+ return null;
6977
+ }
6978
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
6979
+ return parsed.href;
6980
+ }
6981
+ function linkTitle(title, url) {
6982
+ const safeTitle = sanitizeText(title);
6983
+ const href = safeHttpUrl(url);
6984
+ const isTTY = process.stdout.isTTY;
6985
+ const noColor = process.env["NO_COLOR"] !== void 0;
6986
+ if (isTTY && !noColor && href) {
6987
+ return `\x1B]8;;${href}\x1B\\${safeTitle}\x1B]8;;\x1B\\`;
6988
+ }
6989
+ return href ? `${safeTitle} (${href})` : safeTitle;
6990
+ }
6991
+
6931
6992
  // bin/jpi-bounties.js
6932
6993
  var TERMINALHIRE_DIR3 = process.env.TERMINALHIRE_DIR || join4(homedir3(), ".terminalhire");
6933
6994
  var INDEX_CACHE_FILE2 = join4(TERMINALHIRE_DIR3, "index-cache.json");
@@ -6974,12 +7035,6 @@ function formatAmount(b) {
6974
7035
  return b.amountUSD != null ? "$" + b.amountUSD.toLocaleString() : "$\u2014";
6975
7036
  }
6976
7037
  var EFFORT_LABEL = { small: "small (~\xBD day)", medium: "medium (~1 day)", large: "large (multi-day)" };
6977
- function linkTitle(title, url) {
6978
- const isTTY = process.stdout.isTTY;
6979
- const noColor = process.env["NO_COLOR"] !== void 0;
6980
- if (isTTY && !noColor && url) return `\x1B]8;;${url}\x1B\\${title}\x1B]8;;\x1B\\`;
6981
- return url ? `${title} (${url})` : title;
6982
- }
6983
7038
  function printBounty(i, job, score, reason, matchedTags) {
6984
7039
  const b = job.bounty ?? {};
6985
7040
  const stars = b.repoStars != null ? ` \xB7 ${b.repoStars}\u2605` : "";
@@ -6990,11 +7045,11 @@ function printBounty(i, job, score, reason, matchedTags) {
6990
7045
  const ref = opportunityShortToken(job.id);
6991
7046
  console.log(`
6992
7047
  ${i + 1}. ${linkTitle(job.title, job.url)} [${ref}]`);
6993
- console.log(` ${formatAmount(b)}${effort} \xB7 ${b.repoFullName ?? job.company}${stars}${scoreStr}${contend}`);
7048
+ console.log(` ${formatAmount(b)}${effort} \xB7 ${sanitizeText(b.repoFullName ?? job.company)}${stars}${scoreStr}${contend}`);
6994
7049
  if (reason) console.log(` ${reason}`);
6995
7050
  if (matchedTags && matchedTags.length) console.log(` Tags matched: ${matchedTags.slice(0, 5).join(", ")}`);
6996
7051
  console.log(` id: ${job.id}`);
6997
- console.log(` Claim: ${b.claimUrl ?? job.url}`);
7052
+ console.log(` Claim: ${sanitizeText(b.claimUrl ?? job.url)}`);
6998
7053
  console.log(` \u2192 terminalhire claim ${ref}`);
6999
7054
  }
7000
7055
  async function run() {
@@ -7050,7 +7105,7 @@ Enter a number to open a bounty's claim page, or press Enter to exit: `);
7050
7105
  console.log(
7051
7106
  `
7052
7107
  Open this to claim/work the bounty (you go straight to the source \u2014 we never touch payment):
7053
- ${chosen.bounty?.claimUrl ?? chosen.url}`
7108
+ ${sanitizeText(chosen.bounty?.claimUrl ?? chosen.url)}`
7054
7109
  );
7055
7110
  } catch (err) {
7056
7111
  console.error("terminalhire bounties error:", err.message ?? err);
@@ -559,6 +559,13 @@ var init_concurrency = __esm({
559
559
  }
560
560
  });
561
561
 
562
+ // ../../packages/core/src/feeds/effort.ts
563
+ var init_effort = __esm({
564
+ "../../packages/core/src/feeds/effort.ts"() {
565
+ "use strict";
566
+ }
567
+ });
568
+
562
569
  // ../../packages/core/src/feeds/github-bounties.ts
563
570
  var init_github_bounties = __esm({
564
571
  "../../packages/core/src/feeds/github-bounties.ts"() {
@@ -568,6 +575,7 @@ var init_github_bounties = __esm({
568
575
  init_bounty_gate();
569
576
  init_http();
570
577
  init_concurrency();
578
+ init_effort();
571
579
  }
572
580
  });
573
581
 
@@ -579,6 +587,7 @@ var init_opire = __esm({
579
587
  init_bounty_gate();
580
588
  init_github_bounties();
581
589
  init_http();
590
+ init_effort();
582
591
  }
583
592
  });
584
593
 
@@ -3925,9 +3934,9 @@ var init_keytar = __esm({
3925
3934
  }
3926
3935
  });
3927
3936
 
3928
- // node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/rel-0180/node_modules/keytar/build/Release/keytar.node
3937
+ // node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node
3929
3938
  var require_keytar = __commonJS({
3930
- "node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/rel-0180/node_modules/keytar/build/Release/keytar.node"(exports, module) {
3939
+ "node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node"(exports, module) {
3931
3940
  "use strict";
3932
3941
  init_keytar();
3933
3942
  try {
@@ -576,6 +576,13 @@ var init_concurrency = __esm({
576
576
  }
577
577
  });
578
578
 
579
+ // ../../packages/core/src/feeds/effort.ts
580
+ var init_effort = __esm({
581
+ "../../packages/core/src/feeds/effort.ts"() {
582
+ "use strict";
583
+ }
584
+ });
585
+
579
586
  // ../../packages/core/src/feeds/github-bounties.ts
580
587
  var init_github_bounties = __esm({
581
588
  "../../packages/core/src/feeds/github-bounties.ts"() {
@@ -585,6 +592,7 @@ var init_github_bounties = __esm({
585
592
  init_bounty_gate();
586
593
  init_http();
587
594
  init_concurrency();
595
+ init_effort();
588
596
  }
589
597
  });
590
598
 
@@ -596,6 +604,7 @@ var init_opire = __esm({
596
604
  init_bounty_gate();
597
605
  init_github_bounties();
598
606
  init_http();
607
+ init_effort();
599
608
  }
600
609
  });
601
610
 
@@ -3953,9 +3962,9 @@ var init_keytar = __esm({
3953
3962
  }
3954
3963
  });
3955
3964
 
3956
- // node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/rel-0180/node_modules/keytar/build/Release/keytar.node
3965
+ // node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node
3957
3966
  var require_keytar = __commonJS({
3958
- "node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/rel-0180/node_modules/keytar/build/Release/keytar.node"(exports, module) {
3967
+ "node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node"(exports, module) {
3959
3968
  "use strict";
3960
3969
  init_keytar();
3961
3970
  try {
@@ -385,6 +385,11 @@ ${body}`;
385
385
  for (const tok of tokens) {
386
386
  const r = resolveToken(tok);
387
387
  if (!r) continue;
388
+ const synGate = AMBIGUOUS_SYNONYM[tok] ?? AMBIGUOUS_SYNONYM[tok.replace(/^[.\-+#]+|[.\-+#]+$/g, "")];
389
+ if (synGate && r.id === synGate.id) {
390
+ if (synGate.cue.test(text)) ids.add(r.id);
391
+ continue;
392
+ }
388
393
  if (NON_EXTRACTABLE.has(r.id)) continue;
389
394
  if (SYNONYM_ONLY.has(r.id) && !r.viaSynonym) continue;
390
395
  const cue = AMBIGUOUS[r.id];
@@ -402,7 +407,7 @@ ${body}`;
402
407
  function coreTagsFromTitle(title) {
403
408
  return extractSkillTags(title, "").filter((t) => !SOFT_DOMAIN.has(t));
404
409
  }
405
- var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, ENG_INTENT, NON_ENG_TITLE;
410
+ var SOFT_DOMAIN, SYNONYM_ONLY, NON_EXTRACTABLE, AMBIGUOUS, AMBIGUOUS_SYNONYM, ENG_INTENT, NON_ENG_TITLE;
406
411
  var init_extract = __esm({
407
412
  "../../packages/core/src/vocab/extract.ts"() {
408
413
  "use strict";
@@ -439,6 +444,16 @@ var init_extract = __esm({
439
444
  r: /\b(rstudio|tidyverse|ggplot|shiny|dplyr|cran|r-lang|rlang)\b/i,
440
445
  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
441
446
  };
447
+ AMBIGUOUS_SYNONYM = {
448
+ // "prompt" is the LLM skill only in AI context. In raw JD prose it is "prompt
449
+ // delivery / response / payment / communication / attention". Accept only with
450
+ // the explicit "prompt engineering" phrase OR an LLM/AI ecosystem cue; the cue
451
+ // deliberately excludes the bare word "prompt(s)" itself so it can't self-satisfy.
452
+ prompt: {
453
+ id: "prompt-engineering",
454
+ 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
455
+ }
456
+ };
442
457
  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;
443
458
  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;
444
459
  }
@@ -894,6 +909,13 @@ var init_concurrency = __esm({
894
909
  }
895
910
  });
896
911
 
912
+ // ../../packages/core/src/feeds/effort.ts
913
+ var init_effort = __esm({
914
+ "../../packages/core/src/feeds/effort.ts"() {
915
+ "use strict";
916
+ }
917
+ });
918
+
897
919
  // ../../packages/core/src/feeds/github-bounties.ts
898
920
  var init_github_bounties = __esm({
899
921
  "../../packages/core/src/feeds/github-bounties.ts"() {
@@ -903,6 +925,7 @@ var init_github_bounties = __esm({
903
925
  init_bounty_gate();
904
926
  init_http();
905
927
  init_concurrency();
928
+ init_effort();
906
929
  }
907
930
  });
908
931
 
@@ -914,6 +937,7 @@ var init_opire = __esm({
914
937
  init_bounty_gate();
915
938
  init_github_bounties();
916
939
  init_http();
940
+ init_effort();
917
941
  }
918
942
  });
919
943
 
@@ -1242,9 +1266,9 @@ var init_keytar = __esm({
1242
1266
  }
1243
1267
  });
1244
1268
 
1245
- // node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/rel-0180/node_modules/keytar/build/Release/keytar.node
1269
+ // node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node
1246
1270
  var require_keytar = __commonJS({
1247
- "node-file:/Users/ericgang/job-placement-inline/.claude/worktrees/rel-0180/node_modules/keytar/build/Release/keytar.node"(exports, module) {
1271
+ "node-file:/Users/ericgang/job-placement-inline/node_modules/keytar/build/Release/keytar.node"(exports, module) {
1248
1272
  "use strict";
1249
1273
  init_keytar();
1250
1274
  try {
@@ -1608,6 +1632,37 @@ function isContributeEnabled() {
1608
1632
  return readConfig().contributeEnabled === true;
1609
1633
  }
1610
1634
 
1635
+ // bin/sanitize.js
1636
+ var CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
1637
+ function sanitizeText(s) {
1638
+ if (s == null) return "";
1639
+ return String(s).replace(CONTROL_CHARS, "");
1640
+ }
1641
+ function safeHttpUrl(url) {
1642
+ if (url == null) return null;
1643
+ const raw = String(url);
1644
+ CONTROL_CHARS.lastIndex = 0;
1645
+ if (CONTROL_CHARS.test(raw)) return null;
1646
+ let parsed;
1647
+ try {
1648
+ parsed = new URL(raw);
1649
+ } catch {
1650
+ return null;
1651
+ }
1652
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
1653
+ return parsed.href;
1654
+ }
1655
+ function linkTitle(title, url) {
1656
+ const safeTitle = sanitizeText(title);
1657
+ const href = safeHttpUrl(url);
1658
+ const isTTY = process.stdout.isTTY;
1659
+ const noColor = process.env["NO_COLOR"] !== void 0;
1660
+ if (isTTY && !noColor && href) {
1661
+ return `\x1B]8;;${href}\x1B\\${safeTitle}\x1B]8;;\x1B\\`;
1662
+ }
1663
+ return href ? `${safeTitle} (${href})` : safeTitle;
1664
+ }
1665
+
1611
1666
  // bin/jpi-contribute.js
1612
1667
  var TERMINALHIRE_DIR4 = process.env.TERMINALHIRE_DIR || join5(homedir4(), ".terminalhire");
1613
1668
  var INDEX_CACHE_FILE2 = join5(TERMINALHIRE_DIR4, "index-cache.json");
@@ -1709,19 +1764,13 @@ function displayLanguage(job) {
1709
1764
  const tag = (job.tags ?? []).find((t) => LANGUAGES.has(String(t).toLowerCase()));
1710
1765
  return tag ?? "\u2014";
1711
1766
  }
1712
- function linkTitle(title, url) {
1713
- const isTTY = process.stdout.isTTY;
1714
- const noColor = process.env["NO_COLOR"] !== void 0;
1715
- if (isTTY && !noColor && url) return `\x1B]8;;${url}\x1B\\${title}\x1B]8;;\x1B\\`;
1716
- return url ? `${title} (${url})` : title;
1717
- }
1718
1767
  function renderRow(i, result) {
1719
1768
  const job = result.job;
1720
1769
  const c = job.contribution ?? {};
1721
- const repo = c.repoFullName ?? job.company ?? "";
1770
+ const repo = sanitizeText(c.repoFullName ?? job.company ?? "");
1722
1771
  const num = c.issueNumber != null ? `#${c.issueNumber}` : "";
1723
- const label = c.labels && c.labels.length ? c.labels[0] : "\u2014";
1724
- const lang = displayLanguage(job);
1772
+ const label = c.labels && c.labels.length ? sanitizeText(c.labels[0]) : "\u2014";
1773
+ const lang = sanitizeText(displayLanguage(job));
1725
1774
  const scorePct = `match ${Math.round((result.score ?? 0) * 100)}%`;
1726
1775
  const ref = opportunityShortToken(job.id);
1727
1776
  const line1 = `${i + 1}. ${linkTitle(job.title, c.issueUrl ?? job.url)} [${ref}]`;