terminalhire 0.18.2 → 0.19.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # terminalhire — local-first job matching for developers
1
+ # terminalhire — your code is your résumé
2
2
 
3
- Pull curated job matches from a broad public pool. Matching runs entirely on your device. Your profile never leaves your machine.
3
+ Local-first job + bounty matching, and a verifiable **Proof of Work** credential earned from PRs that maintainers who aren't you merged into established repos — checkable on GitHub, impossible to self-report. Matching runs entirely on your device. Your profile never leaves your machine.
4
4
 
5
5
  **Domain:** [terminalhire.com](https://terminalhire.com)
6
6
 
@@ -47,6 +47,7 @@ terminalhire jobs --all # show all matches above zero score
47
47
 
48
48
  terminalhire bounties # day-sized paid tasks you can knock out today
49
49
  terminalhire bounties --priced # only bounties with a known $ amount
50
+ terminalhire contribute # credential-building issues matched to your stack — merges grow your Proof of Work
50
51
 
51
52
  terminalhire claim record <id|issueUrl> # claim a bounty locally + print the executor brief
52
53
  terminalhire claim list --active # list your active claims + accepted-PR rate
@@ -56,6 +57,11 @@ terminalhire trajectory # trajectory from your local Claude Code corpus
56
57
  terminalhire trajectory --export # write a derived score + Markdown to ~/.terminalhire/
57
58
  terminalhire trajectory --inward # also show private rework/recovery (never exported)
58
59
 
60
+ terminalhire devs # opted-in peers & founders, matched 100% locally
61
+ terminalhire intro <login> # request a double-opt-in intro (typed-yes consent)
62
+ terminalhire inbox # unread chats, connections & pending intros in one place
63
+ terminalhire mcp --print-config # read-only MCP server config for VS Code/Cursor/Codex/Zed/JetBrains
64
+
59
65
  terminalhire profile --show # inspect your encrypted local profile (incl. GitHub fields)
60
66
  terminalhire profile --edit # set displayName, contactEmail, prefs
61
67
  terminalhire profile --delete # wipe profile and encryption key from disk
@@ -1181,9 +1181,26 @@ function harmonicMean(a, b) {
1181
1181
  if (a <= 0 || b <= 0) return 0;
1182
1182
  return 2 * a * b / (a + b);
1183
1183
  }
1184
+ function mergeSoftCoverage(covMap, softTags, cap) {
1185
+ for (const st of softTags) {
1186
+ const w = Math.max(0, Math.min(1, st.weight)) * cap;
1187
+ for (const [tag, hit] of expandWeighted([st.tag])) {
1188
+ const scaled = hit.weight * w;
1189
+ const existing = covMap.get(tag);
1190
+ if (!existing || scaled > existing.weight) {
1191
+ covMap.set(tag, {
1192
+ weight: existing ? Math.max(existing.weight, scaled) : scaled,
1193
+ via: existing?.via ?? st.tag
1194
+ });
1195
+ }
1196
+ }
1197
+ }
1198
+ return covMap;
1199
+ }
1184
1200
  function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
1185
1201
  const idfOf = backgroundIdf;
1186
1202
  const expanded = expandWeighted(fp.skillTags);
1203
+ const covMap = opts.softTags && opts.softTags.length > 0 ? mergeSoftCoverage(new Map(expanded), opts.softTags, INTEREST_CAP) : expanded;
1187
1204
  const maxDevScore = fp.skillTags.reduce((acc, t) => acc + idfOf(t), 0);
1188
1205
  const candidates = jobs.filter((j) => passesFilters(fp, j));
1189
1206
  const scored = candidates.map((job) => {
@@ -1194,12 +1211,13 @@ function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
1194
1211
  for (const tag of job.tags) {
1195
1212
  const w = idfOf(tag);
1196
1213
  jobMaxScore += w;
1197
- const hit = expanded.get(tag);
1198
- if (hit) {
1199
- const credit = Math.pow(hit.weight, SHARPEN);
1200
- jobMatchScore += w * credit;
1201
- details.push({ tag, weight: hit.weight, via: hit.via });
1202
- if (credit > (devCovByTag.get(hit.via) ?? 0)) devCovByTag.set(hit.via, credit);
1214
+ const covHit = covMap.get(tag);
1215
+ if (covHit) jobMatchScore += w * Math.pow(covHit.weight, SHARPEN);
1216
+ const fpHit = expanded.get(tag);
1217
+ if (fpHit) {
1218
+ const credit = Math.pow(fpHit.weight, SHARPEN);
1219
+ details.push({ tag, weight: fpHit.weight, via: fpHit.via });
1220
+ if (credit > (devCovByTag.get(fpHit.via) ?? 0)) devCovByTag.set(fpHit.via, credit);
1203
1221
  }
1204
1222
  }
1205
1223
  let devScore = 0;
@@ -1237,7 +1255,7 @@ function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
1237
1255
  return byScore;
1238
1256
  }).slice(0, limit);
1239
1257
  }
1240
- var MIN_SCORE, TIEBREAK_EPS, SHARPEN, CORE_MISS_PENALTY, SENIORITY_RANK, SENIORITY_PATTERNS, ENG_TITLE, UNKNOWN_RECENCY;
1258
+ var MIN_SCORE, TIEBREAK_EPS, SHARPEN, CORE_MISS_PENALTY, INTEREST_CAP, SENIORITY_RANK, SENIORITY_PATTERNS, ENG_TITLE, UNKNOWN_RECENCY;
1241
1259
  var init_matcher = __esm({
1242
1260
  "../../packages/core/src/matcher.ts"() {
1243
1261
  "use strict";
@@ -1247,6 +1265,7 @@ var init_matcher = __esm({
1247
1265
  TIEBREAK_EPS = 5e-3;
1248
1266
  SHARPEN = 1.6;
1249
1267
  CORE_MISS_PENALTY = 0.4;
1268
+ INTEREST_CAP = 0.6;
1250
1269
  SENIORITY_RANK = {
1251
1270
  junior: 0,
1252
1271
  mid: 1,
@@ -1264,6 +1283,54 @@ var init_matcher = __esm({
1264
1283
  }
1265
1284
  });
1266
1285
 
1286
+ // ../../packages/core/src/rerank.ts
1287
+ function jaccardSim(a, b) {
1288
+ if (a.size === 0 && b.size === 0) return 1;
1289
+ let inter = 0;
1290
+ for (const x of a) if (b.has(x)) inter++;
1291
+ const union = a.size + b.size - inter;
1292
+ return union === 0 ? 1 : inter / union;
1293
+ }
1294
+ function tagDissimilarity(a, b) {
1295
+ return 1 - jaccardSim(new Set(a.job.tags ?? []), new Set(b.job.tags ?? []));
1296
+ }
1297
+ function mmrRerank(results, opts = {}) {
1298
+ const lambda = opts.lambda ?? 0.7;
1299
+ const k = opts.k ?? 8;
1300
+ const simOf = opts.simOf ?? tagDissimilarity;
1301
+ if (results.length < 2 || k < 2) return results.slice();
1302
+ let maxScore = 0;
1303
+ for (const r of results) if (r.score > maxScore) maxScore = r.score;
1304
+ const relNorm = (r) => maxScore > 0 ? r.score / maxScore : 0;
1305
+ const remaining = results.slice();
1306
+ const selected = [];
1307
+ const window = Math.min(k, remaining.length);
1308
+ for (let pos = 0; pos < window; pos++) {
1309
+ let bestIdx = 0;
1310
+ let bestObj = -Infinity;
1311
+ for (let i = 0; i < remaining.length; i++) {
1312
+ const cand = remaining[i];
1313
+ let minDissim = selected.length === 0 ? 0 : Infinity;
1314
+ for (const s of selected) {
1315
+ const d = simOf(cand, s);
1316
+ if (d < minDissim) minDissim = d;
1317
+ }
1318
+ const obj = lambda * relNorm(cand) + (1 - lambda) * minDissim;
1319
+ if (obj > bestObj) {
1320
+ bestObj = obj;
1321
+ bestIdx = i;
1322
+ }
1323
+ }
1324
+ selected.push(remaining.splice(bestIdx, 1)[0]);
1325
+ }
1326
+ return [...selected, ...remaining];
1327
+ }
1328
+ var init_rerank = __esm({
1329
+ "../../packages/core/src/rerank.ts"() {
1330
+ "use strict";
1331
+ }
1332
+ });
1333
+
1267
1334
  // ../../packages/core/src/feeds/http.ts
1268
1335
  function fetchWithTimeout(input, init, timeoutMs = FEED_FETCH_TIMEOUT_MS) {
1269
1336
  return fetch(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
@@ -2675,6 +2742,55 @@ var init_feeds = __esm({
2675
2742
  }
2676
2743
  });
2677
2744
 
2745
+ // ../../packages/core/src/feeds/contribution-classify.ts
2746
+ function hasStrongCodeSignal(title, body, labels) {
2747
+ if (labels.some((l) => CODE_LABEL_RE.test(l))) return true;
2748
+ const text = `${title}
2749
+ ${body}`;
2750
+ if (CODE_TERM_RE.test(text)) return true;
2751
+ if (CODE_EXCEPTION_RE.test(text)) return true;
2752
+ if (CODE_FENCE_RE.test(body)) return true;
2753
+ if (FILE_PATH_RE.test(text)) return true;
2754
+ return false;
2755
+ }
2756
+ function hasContentSignal(title, body, labels) {
2757
+ if (labels.some((l) => CONTENT_LABEL_RE.test(l))) return true;
2758
+ const text = `${title}
2759
+ ${body}`;
2760
+ if (CONTENT_ADD_RE.test(text)) return true;
2761
+ if (ADD_TO_CORPUS_RE.test(text)) return true;
2762
+ if (TRANSLATE_RE.test(text)) return true;
2763
+ if (TYPO_RE.test(text)) return true;
2764
+ return false;
2765
+ }
2766
+ function classifyContributionKind(input) {
2767
+ const title = input.title ?? "";
2768
+ const body = input.body ?? "";
2769
+ const labels = input.labels ?? [];
2770
+ if (hasStrongCodeSignal(title, body, labels)) return "code";
2771
+ if (hasContentSignal(title, body, labels)) return "content";
2772
+ return "ambiguous";
2773
+ }
2774
+ function looksLikeContentTask(input) {
2775
+ return classifyContributionKind(input) === "content";
2776
+ }
2777
+ var CONTENT_LABEL_RE, CODE_LABEL_RE, CODE_TERM_RE, CODE_EXCEPTION_RE, CODE_FENCE_RE, FILE_PATH_RE, CONTENT_ADD_RE, ADD_TO_CORPUS_RE, TRANSLATE_RE, TYPO_RE;
2778
+ var init_contribution_classify = __esm({
2779
+ "../../packages/core/src/feeds/contribution-classify.ts"() {
2780
+ "use strict";
2781
+ CONTENT_LABEL_RE = /\b(content|copy|copywriting|wording|translation|translations|i18n|l10n|localization|localisation|data|dataset|documentation|docs)\b/i;
2782
+ CODE_LABEL_RE = /\b(bug|bugfix|fix|enhancement|feature|refactor|refactoring|test|tests|testing|performance|perf|security|api|backend|frontend|typescript|javascript|golang|rust|python|build|ci)\b/i;
2783
+ CODE_TERM_RE = /\b(bug|crash|crashes|crashing|exception|stack\s?trace|stacktrace|null\s?pointer|npe|segfault|refactor|implement|endpoint|api|component|function|method|class|module|compile|compiler|build\s+(?:error|fail)|runtime|regression|unit\s+test|integration\s+test|test\s+coverage|typecheck|lint|dependency|dependencies|import|async|await|race\s+condition|memory\s+leak|deadlock|parser|serialize|deserialize|schema|migration|websocket|http|json|sql|cli|sdk)\b/i;
2784
+ CODE_EXCEPTION_RE = /exception|stacktrace|segfault|traceback/i;
2785
+ CODE_FENCE_RE = /```|(?:^|\n)\s{4,}\S/;
2786
+ FILE_PATH_RE = /\b[\w./-]+\.(?:ts|tsx|js|jsx|mjs|cjs|py|rb|go|rs|java|kt|scala|c|cc|cpp|cxx|h|hpp|cs|php|swift|m|mm|sh|bash|zsh|sql|graphql|proto|css|scss|sass|less|vue|svelte|toml|ini|gradle|dockerfile)\b/i;
2787
+ CONTENT_ADD_RE = /\badd(?:ing|s)?\s+(?:\w+\s+){0,4}?(?:proverbs?|words?|phrases?|sayings?|quotes?|quotations?|translations?|entry|entries|definitions?|terms?|idioms?|synonyms?|antonyms?|acronyms?|abbreviations?)\b/i;
2788
+ ADD_TO_CORPUS_RE = /\badd\b[\s\S]*?\bto\s+(?:the\s+)?(?:word\s?list|dictionary|glossary|phrasebook)\b/i;
2789
+ TRANSLATE_RE = /\b(?:translate|translating|translation|localize|localise|localization|localisation)\b/i;
2790
+ TYPO_RE = /\bfix(?:ing)?\s+(?:a\s+|the\s+|some\s+)?typos?\b/i;
2791
+ }
2792
+ });
2793
+
2678
2794
  // ../../packages/core/src/feeds/contributions.ts
2679
2795
  function authHeaders2() {
2680
2796
  const token = process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"];
@@ -2731,11 +2847,11 @@ async function contributorCount(client, fullName) {
2731
2847
  }
2732
2848
  }
2733
2849
  async function openPRIssueRefs(client, fullName) {
2734
- const refs = /* @__PURE__ */ new Set();
2735
2850
  const prs = await client.json(
2736
2851
  `/repos/${fullName}/pulls?state=open&per_page=100`
2737
2852
  );
2738
- if (!Array.isArray(prs)) return refs;
2853
+ if (!Array.isArray(prs)) return null;
2854
+ const refs = /* @__PURE__ */ new Set();
2739
2855
  for (const pr of prs) {
2740
2856
  for (const m of `${pr.title ?? ""}
2741
2857
  ${pr.body ?? ""}`.matchAll(/#(\d+)\b/g)) {
@@ -2780,8 +2896,7 @@ async function aggregateContributions(opts = {}) {
2780
2896
  return n;
2781
2897
  }
2782
2898
  async function repoPRRefs(fullName) {
2783
- const hit = prRefsCache.get(fullName);
2784
- if (hit !== void 0) return hit;
2899
+ if (prRefsCache.has(fullName)) return prRefsCache.get(fullName) ?? null;
2785
2900
  const refs = await openPRIssueRefs(client, fullName);
2786
2901
  prRefsCache.set(fullName, refs);
2787
2902
  return refs;
@@ -2813,10 +2928,12 @@ async function aggregateContributions(opts = {}) {
2813
2928
  continue;
2814
2929
  }
2815
2930
  if (repo.disabled) continue;
2816
- const prRefs = await repoPRRefs(fullName);
2817
- if (prRefs.has(issue.number)) continue;
2818
2931
  const body = issue.body ? decodeEntities(issue.body) : "";
2819
2932
  const labels = labelNames2(issue.labels);
2933
+ if (looksLikeContentTask({ title, body, labels })) continue;
2934
+ const prRefs = await repoPRRefs(fullName);
2935
+ if (prRefs && prRefs.has(issue.number)) continue;
2936
+ const openPRsAtDiscovery = prRefs ? 0 : void 0;
2820
2937
  const tags = normalize(
2821
2938
  tokenize4([title, repo.language ?? "", labels.join(" "), body.slice(0, 2e3)].join(" "))
2822
2939
  );
@@ -2842,7 +2959,14 @@ async function aggregateContributions(opts = {}) {
2842
2959
  issueNumber: issue.number,
2843
2960
  labels,
2844
2961
  issueUrl: issue.html_url,
2845
- issueBody: body.slice(0, 1e3) || void 0
2962
+ issueBody: body.slice(0, 1e3) || void 0,
2963
+ // Provably 0 open PRs at discovery ONLY when the open-PR check actually
2964
+ // ran and returned a verified-empty/non-matching set (`openPRsAtDiscovery
2965
+ // === 0`). When the check FAILED (rate-limit/error → `prRefs === null`)
2966
+ // this is `undefined`, NOT a fabricated 0 — the claim path then falls
2967
+ // through to a live re-count / honest "unknown" (jpi-claim.js:242-260)
2968
+ // instead of persisting an unverified "0 open PRs" as fact.
2969
+ openPRsAtDiscovery
2846
2970
  },
2847
2971
  raw: issue
2848
2972
  });
@@ -2857,6 +2981,7 @@ var init_contributions = __esm({
2857
2981
  init_entities();
2858
2982
  init_bounty_gate();
2859
2983
  init_contribution_gate();
2984
+ init_contribution_classify();
2860
2985
  init_github_bounties();
2861
2986
  init_http();
2862
2987
  GITHUB_API2 = "https://api.github.com";
@@ -6194,7 +6319,7 @@ function funnelCounts(map) {
6194
6319
  for (const key of Object.keys(map)) {
6195
6320
  const rec = map[key];
6196
6321
  if (rec.clicked === true) counts.clicked++;
6197
- if (rec.status) counts[rec.status]++;
6322
+ if (rec.status && Object.prototype.hasOwnProperty.call(counts, rec.status)) counts[rec.status]++;
6198
6323
  }
6199
6324
  return counts;
6200
6325
  }
@@ -6511,6 +6636,7 @@ __export(src_exports, {
6511
6636
  GRAPH: () => GRAPH,
6512
6637
  GREENHOUSE_SLUGS_BY_TIER: () => GREENHOUSE_SLUGS_BY_TIER,
6513
6638
  IDF_BACKGROUND: () => IDF_BACKGROUND,
6639
+ INTEREST_CAP: () => INTEREST_CAP,
6514
6640
  INTRO_ACCEPTED_TTL_MS: () => INTRO_ACCEPTED_TTL_MS,
6515
6641
  INTRO_ALLOWED_FIELDS: () => INTRO_ALLOWED_FIELDS,
6516
6642
  INTRO_PENDING_TTL_MS: () => INTRO_PENDING_TTL_MS,
@@ -6580,6 +6706,7 @@ __export(src_exports, {
6580
6706
  loadPartnerRoles: () => loadPartnerRoles,
6581
6707
  looksLikeEngRole: () => looksLikeEngRole,
6582
6708
  match: () => match,
6709
+ mmrRerank: () => mmrRerank,
6583
6710
  normalize: () => normalize,
6584
6711
  opire: () => opire,
6585
6712
  opportunityShortToken: () => opportunityShortToken,
@@ -6595,6 +6722,7 @@ __export(src_exports, {
6595
6722
  sameLogin: () => sameLogin,
6596
6723
  setStatus: () => setStatus,
6597
6724
  signalLabel: () => signalLabel,
6725
+ tagDissimilarity: () => tagDissimilarity,
6598
6726
  tokenize: () => tokenize,
6599
6727
  validateGraph: () => validateGraph,
6600
6728
  validateIntroPayload: () => validateIntroPayload,
@@ -6608,6 +6736,7 @@ var init_src = __esm({
6608
6736
  init_types();
6609
6737
  init_vocabulary();
6610
6738
  init_matcher();
6739
+ init_rerank();
6611
6740
  init_feeds();
6612
6741
  init_indexer();
6613
6742
  init_partners();
@@ -482,6 +482,13 @@ var init_matcher = __esm({
482
482
  }
483
483
  });
484
484
 
485
+ // ../../packages/core/src/rerank.ts
486
+ var init_rerank = __esm({
487
+ "../../packages/core/src/rerank.ts"() {
488
+ "use strict";
489
+ }
490
+ });
491
+
485
492
  // ../../packages/core/src/feeds/http.ts
486
493
  var init_http = __esm({
487
494
  "../../packages/core/src/feeds/http.ts"() {
@@ -736,6 +743,13 @@ var init_feeds = __esm({
736
743
  }
737
744
  });
738
745
 
746
+ // ../../packages/core/src/feeds/contribution-classify.ts
747
+ var init_contribution_classify = __esm({
748
+ "../../packages/core/src/feeds/contribution-classify.ts"() {
749
+ "use strict";
750
+ }
751
+ });
752
+
739
753
  // ../../packages/core/src/feeds/contributions.ts
740
754
  var init_contributions = __esm({
741
755
  "../../packages/core/src/feeds/contributions.ts"() {
@@ -744,6 +758,7 @@ var init_contributions = __esm({
744
758
  init_entities();
745
759
  init_bounty_gate();
746
760
  init_contribution_gate();
761
+ init_contribution_classify();
747
762
  init_github_bounties();
748
763
  init_http();
749
764
  }
@@ -3912,6 +3927,7 @@ var init_src = __esm({
3912
3927
  init_types();
3913
3928
  init_vocabulary();
3914
3929
  init_matcher();
3930
+ init_rerank();
3915
3931
  init_feeds();
3916
3932
  init_indexer();
3917
3933
  init_partners();
@@ -4389,7 +4405,9 @@ var init_config = __esm({
4389
4405
  inboundNudgeMuted: false,
4390
4406
  inboundNudgeDisclosed: false,
4391
4407
  contributeEnabled: false,
4392
- contributePrompted: false
4408
+ contributePrompted: false,
4409
+ betaOptIn: false,
4410
+ lastFullFeedbackAt: null
4393
4411
  };
4394
4412
  }
4395
4413
  });
@@ -499,6 +499,13 @@ var init_matcher = __esm({
499
499
  }
500
500
  });
501
501
 
502
+ // ../../packages/core/src/rerank.ts
503
+ var init_rerank = __esm({
504
+ "../../packages/core/src/rerank.ts"() {
505
+ "use strict";
506
+ }
507
+ });
508
+
502
509
  // ../../packages/core/src/feeds/http.ts
503
510
  var init_http = __esm({
504
511
  "../../packages/core/src/feeds/http.ts"() {
@@ -753,6 +760,13 @@ var init_feeds = __esm({
753
760
  }
754
761
  });
755
762
 
763
+ // ../../packages/core/src/feeds/contribution-classify.ts
764
+ var init_contribution_classify = __esm({
765
+ "../../packages/core/src/feeds/contribution-classify.ts"() {
766
+ "use strict";
767
+ }
768
+ });
769
+
756
770
  // ../../packages/core/src/feeds/contributions.ts
757
771
  var init_contributions = __esm({
758
772
  "../../packages/core/src/feeds/contributions.ts"() {
@@ -761,6 +775,7 @@ var init_contributions = __esm({
761
775
  init_entities();
762
776
  init_bounty_gate();
763
777
  init_contribution_gate();
778
+ init_contribution_classify();
764
779
  init_github_bounties();
765
780
  init_http();
766
781
  }
@@ -3940,6 +3955,7 @@ var init_src = __esm({
3940
3955
  init_types();
3941
3956
  init_vocabulary();
3942
3957
  init_matcher();
3958
+ init_rerank();
3943
3959
  init_feeds();
3944
3960
  init_indexer();
3945
3961
  init_partners();
@@ -4417,7 +4433,9 @@ var init_config = __esm({
4417
4433
  inboundNudgeMuted: false,
4418
4434
  inboundNudgeDisclosed: false,
4419
4435
  contributeEnabled: false,
4420
- contributePrompted: false
4436
+ contributePrompted: false,
4437
+ betaOptIn: false,
4438
+ lastFullFeedbackAt: null
4421
4439
  };
4422
4440
  }
4423
4441
  });