terminalhire 0.35.3 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/bin/claim-push-bg.js +1 -1
  2. package/dist/bin/jpi-bounties.js +120 -19
  3. package/dist/bin/jpi-chat-read.js +28 -8
  4. package/dist/bin/jpi-chat.js +30 -10
  5. package/dist/bin/jpi-claim.js +121 -20
  6. package/dist/bin/jpi-config.js +8 -3
  7. package/dist/bin/jpi-contribute.js +25 -36
  8. package/dist/bin/jpi-devs.js +121 -19
  9. package/dist/bin/jpi-dispatch.js +6496 -501
  10. package/dist/bin/jpi-hub.js +127 -26
  11. package/dist/bin/jpi-inbox.js +30 -10
  12. package/dist/bin/jpi-init.js +118 -17
  13. package/dist/bin/jpi-intro.js +19 -4
  14. package/dist/bin/jpi-jobs.js +129 -23
  15. package/dist/bin/jpi-learn.js +18 -3
  16. package/dist/bin/jpi-link.js +9 -4
  17. package/dist/bin/jpi-login.js +129 -23
  18. package/dist/bin/jpi-mcp-chat.js +27177 -0
  19. package/dist/bin/jpi-mcp.js +5838 -141
  20. package/dist/bin/jpi-profile.js +18 -3
  21. package/dist/bin/jpi-project.js +118 -17
  22. package/dist/bin/jpi-refresh.js +159 -61
  23. package/dist/bin/jpi-repo.js +18 -3
  24. package/dist/bin/jpi-save.js +18 -3
  25. package/dist/bin/jpi-spinner.js +16 -13
  26. package/dist/bin/jpi-sync.js +18 -3
  27. package/dist/bin/jpi-trajectory.js +20 -5
  28. package/dist/bin/peer-connect-prompt.js +8 -3
  29. package/dist/bin/pulse-prompt.js +9 -4
  30. package/dist/bin/spinner.js +16 -15
  31. package/dist/src/chat-client.js +20 -5
  32. package/dist/src/chat-keystore.js +18 -3
  33. package/dist/src/config.js +10 -8
  34. package/dist/src/crypto-store.js +1 -1
  35. package/dist/src/github-auth.js +1 -1
  36. package/dist/src/intro.js +19 -4
  37. package/dist/src/link.js +9 -4
  38. package/dist/src/profile.js +18 -3
  39. package/dist/src/repo-experience.js +18 -3
  40. package/dist/src/reputation/fetch.js +16 -1
  41. package/dist/src/signal.js +16 -1
  42. package/dist/src/trajectory.js +20 -5
  43. package/dist/src/web-session.js +1 -1
  44. package/package.json +2 -2
@@ -1748,6 +1748,32 @@ async function openPRClosingRefs(owner, name, token, signal, governor) {
1748
1748
  return null;
1749
1749
  }
1750
1750
  }
1751
+ async function issueCrossRefPRAttempts(owner, name, issueNumber, token, signal, governor) {
1752
+ const url = `https://api.github.com/repos/${owner}/${name}/issues/${issueNumber}/timeline?per_page=100`;
1753
+ try {
1754
+ const res = governor ? await governor.get(url, { headers: ghHeaders(token), signal }) : await fetch(url, { headers: ghHeaders(token), signal });
1755
+ if (!res || !res.ok) return null;
1756
+ const link = res.headers?.get("link") ?? null;
1757
+ const hasNextPage = link != null && /\brel="next"/.test(link);
1758
+ const events = await res.json();
1759
+ if (!Array.isArray(events)) return null;
1760
+ if (hasNextPage || events.length >= 100) return null;
1761
+ let hasOpenPR = false;
1762
+ let hasClosedPR = false;
1763
+ const prNumbers = [];
1764
+ for (const ev of events) {
1765
+ if (ev?.event !== "cross-referenced") continue;
1766
+ const src = ev.source?.issue;
1767
+ if (!src || src.pull_request == null) continue;
1768
+ if (typeof src.number === "number") prNumbers.push(src.number);
1769
+ if (src.state === "open") hasOpenPR = true;
1770
+ else if (src.state === "closed") hasClosedPR = true;
1771
+ }
1772
+ return { hasOpenPR, hasClosedPR, prNumbers };
1773
+ } catch {
1774
+ return null;
1775
+ }
1776
+ }
1751
1777
  function makeScoringGovernor(governor) {
1752
1778
  return governor ?? makeGitHubGovernor(
1753
1779
  ((url, init) => fetch(url, init)),
@@ -3925,6 +3951,30 @@ var init_feeds = __esm({
3925
3951
  });
3926
3952
 
3927
3953
  // ../../packages/core/src/feeds/contributions.ts
3954
+ function readSearchMaxPages() {
3955
+ const raw = process.env["CONTRIB_SEARCH_MAX_PAGES"];
3956
+ if (raw == null) return DEFAULT_SEARCH_MAX_PAGES;
3957
+ const n = Number.parseInt(raw, 10);
3958
+ if (Number.isNaN(n)) return DEFAULT_SEARCH_MAX_PAGES;
3959
+ return Math.min(Math.max(n, MIN_SEARCH_MAX_PAGES), MAX_SEARCH_MAX_PAGES);
3960
+ }
3961
+ function readMaxContribItems() {
3962
+ const raw = process.env["CONTRIB_MAX_ITEMS"];
3963
+ if (raw == null) return DEFAULT_MAX_CONTRIB_ITEMS;
3964
+ const n = Number.parseInt(raw, 10);
3965
+ if (Number.isNaN(n)) return DEFAULT_MAX_CONTRIB_ITEMS;
3966
+ return Math.min(Math.max(n, MIN_MAX_CONTRIB_ITEMS), MAX_MAX_CONTRIB_ITEMS);
3967
+ }
3968
+ function readMaxContribIssuesScanned() {
3969
+ const raw = process.env["CONTRIB_MAX_ISSUES_SCANNED"];
3970
+ if (raw == null) return DEFAULT_MAX_CONTRIB_ISSUES_SCANNED;
3971
+ const n = Number.parseInt(raw, 10);
3972
+ if (Number.isNaN(n)) return DEFAULT_MAX_CONTRIB_ISSUES_SCANNED;
3973
+ return Math.min(
3974
+ Math.max(n, MIN_MAX_CONTRIB_ISSUES_SCANNED),
3975
+ MAX_MAX_CONTRIB_ISSUES_SCANNED
3976
+ );
3977
+ }
3928
3978
  function authHeaders2() {
3929
3979
  const token = process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"];
3930
3980
  const h = {
@@ -4012,13 +4062,21 @@ async function fetchRateLimit(client) {
4012
4062
  }
4013
4063
  async function searchContribIssues(client, queries) {
4014
4064
  const byUrl = /* @__PURE__ */ new Map();
4065
+ const maxPages = readSearchMaxPages();
4015
4066
  for (const q of queries) {
4016
- const res = await client.json(
4017
- `/search/issues?q=${encodeURIComponent(q)}&sort=created&order=desc&per_page=${SEARCH_PER_PAGE2}`
4018
- );
4019
- for (const it of res?.items ?? []) {
4020
- if (it.pull_request) continue;
4021
- if (!byUrl.has(it.html_url)) byUrl.set(it.html_url, it);
4067
+ for (let page = 1; page <= maxPages; page++) {
4068
+ const res = await client.json(
4069
+ `/search/issues?q=${encodeURIComponent(q)}&sort=created&order=desc&per_page=${SEARCH_PER_PAGE2}&page=${page}`
4070
+ );
4071
+ const items = res?.items;
4072
+ if (items == null) break;
4073
+ for (const it of items) {
4074
+ if (it.pull_request) continue;
4075
+ if (!byUrl.has(it.html_url)) byUrl.set(it.html_url, it);
4076
+ }
4077
+ if (items.length < SEARCH_PER_PAGE2) break;
4078
+ const stats = client.getStats();
4079
+ if (stats.budgetAborted || stats.secondaryAborted) break;
4022
4080
  }
4023
4081
  }
4024
4082
  return [...byUrl.values()].sort(
@@ -4054,7 +4112,12 @@ function buildContributionJob(a) {
4054
4112
  // TERM-27: persist the repo's primary language so project curation can
4055
4113
  // exclude the repo's OWN language id (folded into every issue's tags) from
4056
4114
  // the distinct-skill signal. Same `repo` used by the tokenize() tag build.
4057
- language: a.repo.language ?? null
4115
+ language: a.repo.language ?? null,
4116
+ // TERM-35: stamp the search item's comment count (already in the response —
4117
+ // zero extra egress). A NEUTRAL volume signal only: set solely when the
4118
+ // search item carries a finite count >= 0; a failed/absent value leaves it
4119
+ // undefined (never a fabricated 0), so a render's chip falls through cleanly.
4120
+ commentsAtDiscovery: typeof a.issue.comments === "number" && Number.isFinite(a.issue.comments) && a.issue.comments >= 0 ? a.issue.comments : void 0
4058
4121
  },
4059
4122
  // Provenance: repo-first discovered items only (label-first omits the field).
4060
4123
  ...a.discovered ? { discovered: true } : {},
@@ -4074,10 +4137,14 @@ async function aggregateContributions(opts = {}) {
4074
4137
  probeTimeoutMs: opts.fetchImpl ? null : PROBE_TIMEOUT_MS
4075
4138
  });
4076
4139
  const queries = opts.queries ?? CONTRIB_SEARCH_QUERIES;
4140
+ const maxContribItems = readMaxContribItems();
4077
4141
  const startRl = await fetchRateLimit(client);
4078
4142
  const coreHealthyAtStart = (startRl?.core?.remaining ?? 0) >= 500;
4079
4143
  client.setSecondaryHint(coreHealthyAtStart);
4080
- const issues = (await searchContribIssues(client, queries)).slice(0, MAX_CONTRIB_ISSUES_SCANNED);
4144
+ const issues = (await searchContribIssues(client, queries)).slice(
4145
+ 0,
4146
+ readMaxContribIssuesScanned()
4147
+ );
4081
4148
  const repoCache = /* @__PURE__ */ new Map();
4082
4149
  const contribCache = /* @__PURE__ */ new Map();
4083
4150
  const prRefsCache = /* @__PURE__ */ new Map();
@@ -4167,7 +4234,7 @@ async function aggregateContributions(opts = {}) {
4167
4234
  let contribUndefined = 0;
4168
4235
  let prRefsNull = 0;
4169
4236
  for (const issue of issues) {
4170
- if (jobs.length >= MAX_CONTRIB_ITEMS) break;
4237
+ if (jobs.length >= maxContribItems) break;
4171
4238
  const fullName = repoFullNameFromApiUrl2(issue.repository_url);
4172
4239
  if (!fullName) continue;
4173
4240
  const id = `contribute:${repoKey(fullName)}#${issue.number}`;
@@ -4225,7 +4292,7 @@ async function aggregateContributions(opts = {}) {
4225
4292
  const doDiscovery = opts.discoverRepos ?? (opts.trendingSlugs != null || opts.vocabTerms != null);
4226
4293
  let discoveredEmitted = 0;
4227
4294
  let discoveryBudgetStopped = false;
4228
- if (doDiscovery && jobs.length < MAX_CONTRIB_ITEMS) {
4295
+ if (doDiscovery && jobs.length < maxContribItems) {
4229
4296
  const maxRepos = Math.min(
4230
4297
  Math.max(0, opts.maxDiscoveredRepos ?? MAX_DISCOVERED_REPOS),
4231
4298
  MAX_DISCOVERED_REPOS
@@ -4278,7 +4345,7 @@ async function aggregateContributions(opts = {}) {
4278
4345
  }
4279
4346
  const scanned = candidates.slice(0, maxRepos);
4280
4347
  for (const fullName of scanned) {
4281
- if (jobs.length >= MAX_CONTRIB_ITEMS) break;
4348
+ if (jobs.length >= maxContribItems) break;
4282
4349
  if (client.getStats().budgetAborted || client.getStats().secondaryAborted) {
4283
4350
  discoveryBudgetStopped = true;
4284
4351
  break;
@@ -4303,7 +4370,7 @@ async function aggregateContributions(opts = {}) {
4303
4370
  const repoIssues = (searchRes?.items ?? []).filter((it) => !it.pull_request);
4304
4371
  let perRepoDiscovered = 0;
4305
4372
  for (const issue of repoIssues) {
4306
- if (jobs.length >= MAX_CONTRIB_ITEMS) break;
4373
+ if (jobs.length >= maxContribItems) break;
4307
4374
  if (perRepoDiscovered >= MAX_ISSUES_PER_DISCOVERED_REPO) break;
4308
4375
  if ((perRepo.get(repoKey(fullName)) ?? 0) >= MAX_BOUNTIES_PER_DISCOVERED_REPO) break;
4309
4376
  const id = `contribute:${repoKey(fullName)}#${issue.number}`;
@@ -4368,7 +4435,7 @@ async function aggregateContributions(opts = {}) {
4368
4435
  }
4369
4436
  return jobs;
4370
4437
  }
4371
- var GITHUB_API2, CONTRIB_LABEL_QUERIES, CONTRIB_LANGUAGE_QUERIES, CONTRIB_SEARCH_QUERIES, SEARCH_PER_PAGE2, MAX_CONTRIB_ITEMS, MAX_CONTRIB_ISSUES_SCANNED, MAX_DISCOVERED_REPOS, MAX_ISSUES_PER_DISCOVERED_REPO, DISCOVERY_REPOS_PER_TERM, DISCOVERY_VOCAB_TERMS, DISCOVERY_ISSUE_LABELS, repoKey;
4438
+ var GITHUB_API2, CONTRIB_LABEL_QUERIES, CONTRIB_LANGUAGE_QUERIES, CONTRIB_SEARCH_QUERIES, SEARCH_PER_PAGE2, DEFAULT_SEARCH_MAX_PAGES, MIN_SEARCH_MAX_PAGES, MAX_SEARCH_MAX_PAGES, DEFAULT_MAX_CONTRIB_ITEMS, MIN_MAX_CONTRIB_ITEMS, MAX_MAX_CONTRIB_ITEMS, DEFAULT_MAX_CONTRIB_ISSUES_SCANNED, MIN_MAX_CONTRIB_ISSUES_SCANNED, MAX_MAX_CONTRIB_ISSUES_SCANNED, MAX_DISCOVERED_REPOS, MAX_ISSUES_PER_DISCOVERED_REPO, DISCOVERY_REPOS_PER_TERM, DISCOVERY_VOCAB_TERMS, DISCOVERY_ISSUE_LABELS, repoKey;
4372
4439
  var init_contributions = __esm({
4373
4440
  "../../packages/core/src/feeds/contributions.ts"() {
4374
4441
  "use strict";
@@ -4387,7 +4454,11 @@ var init_contributions = __esm({
4387
4454
  'label:"good-first-issue" type:issue state:open',
4388
4455
  'label:"help wanted" type:issue state:open',
4389
4456
  'label:"help-wanted" type:issue state:open',
4390
- 'label:"up-for-grabs" type:issue state:open'
4457
+ 'label:"up-for-grabs" type:issue state:open',
4458
+ // supply-expansion D: two more first-contribution label families widen the
4459
+ // global newest-first slice WITHOUT relaxing the credential gate.
4460
+ 'label:"beginner-friendly" type:issue state:open',
4461
+ 'label:"first-timers-only" type:issue state:open'
4391
4462
  ];
4392
4463
  CONTRIB_LANGUAGE_QUERIES = [
4393
4464
  ...["rust", "go", "python", "c++", "ruby"].map(
@@ -4395,12 +4466,30 @@ var init_contributions = __esm({
4395
4466
  ),
4396
4467
  ...["rust", "go"].map(
4397
4468
  (lang) => `label:"good first issue" language:${lang} type:issue state:open`
4469
+ ),
4470
+ // supply-expansion D: cover the high-volume web/enterprise ecosystems the
4471
+ // original set omitted. TS/JS were previously left out of "good first issue"
4472
+ // (the global slice over-represented them) but a LANGUAGE-scoped page surfaces
4473
+ // DIFFERENT repos than the global newest-first slice, so re-including them widens
4474
+ // distinct-repo coverage rather than duplicating it.
4475
+ ...["typescript", "javascript", "java", "python"].map(
4476
+ (lang) => `label:"good first issue" language:${lang} type:issue state:open`
4477
+ ),
4478
+ ...["typescript", "javascript", "c#", "php"].map(
4479
+ (lang) => `label:"help wanted" language:${lang} type:issue state:open`
4398
4480
  )
4399
4481
  ];
4400
4482
  CONTRIB_SEARCH_QUERIES = [...CONTRIB_LABEL_QUERIES, ...CONTRIB_LANGUAGE_QUERIES];
4401
4483
  SEARCH_PER_PAGE2 = 100;
4402
- MAX_CONTRIB_ITEMS = 150;
4403
- MAX_CONTRIB_ISSUES_SCANNED = 600;
4484
+ DEFAULT_SEARCH_MAX_PAGES = 1;
4485
+ MIN_SEARCH_MAX_PAGES = 1;
4486
+ MAX_SEARCH_MAX_PAGES = 5;
4487
+ DEFAULT_MAX_CONTRIB_ITEMS = 400;
4488
+ MIN_MAX_CONTRIB_ITEMS = 50;
4489
+ MAX_MAX_CONTRIB_ITEMS = 1e3;
4490
+ DEFAULT_MAX_CONTRIB_ISSUES_SCANNED = 1500;
4491
+ MIN_MAX_CONTRIB_ISSUES_SCANNED = 100;
4492
+ MAX_MAX_CONTRIB_ISSUES_SCANNED = 5e3;
4404
4493
  MAX_DISCOVERED_REPOS = 15;
4405
4494
  MAX_ISSUES_PER_DISCOVERED_REPO = 3;
4406
4495
  DISCOVERY_REPOS_PER_TERM = 20;
@@ -4570,6 +4659,15 @@ async function enrichWinnability(jobs, contribute, w) {
4570
4659
  );
4571
4660
  }
4572
4661
  }
4662
+ function hasClickableUrl(url) {
4663
+ if (!url) return false;
4664
+ try {
4665
+ const parsed = new URL(url);
4666
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
4667
+ } catch {
4668
+ return false;
4669
+ }
4670
+ }
4573
4671
  async function buildIndex(opts) {
4574
4672
  const includePartners = opts?.includePartners ?? true;
4575
4673
  const publicJobs = await aggregate(opts);
@@ -4580,6 +4678,7 @@ async function buildIndex(opts) {
4580
4678
  ...opts?.partnerRoles ?? []
4581
4679
  ];
4582
4680
  for (const job of partnerJobs) {
4681
+ if (!hasClickableUrl(job.url)) continue;
4583
4682
  if (!seen.has(job.id)) {
4584
4683
  seen.add(job.id);
4585
4684
  allJobs.push(job);
@@ -4661,7 +4760,8 @@ async function fetchIssueStatus(fullName, issueNumber, opts = {}) {
4661
4760
  for (const a of body.assignees ?? []) {
4662
4761
  if (a && typeof a.login === "string") assignees.add(a.login);
4663
4762
  }
4664
- return { state, assignees: [...assignees] };
4763
+ const comments = typeof body.comments === "number" && Number.isFinite(body.comments) && body.comments >= 0 ? body.comments : null;
4764
+ return { state, assignees: [...assignees], comments };
4665
4765
  }
4666
4766
  var GITHUB_API3, DEFAULT_ISSUE_STATUS_TIMEOUT_MS;
4667
4767
  var init_github_issue_status = __esm({
@@ -8371,6 +8471,7 @@ __export(src_exports, {
8371
8471
  isOverIntroLimit: () => isOverIntroLimit,
8372
8472
  isTrivialPRTitle: () => isTrivialPRTitle,
8373
8473
  isWinnableIssue: () => isWinnableIssue,
8474
+ issueCrossRefPRAttempts: () => issueCrossRefPRAttempts,
8374
8475
  jobShortToken: () => jobShortToken,
8375
8476
  joinLabels: () => joinLabels,
8376
8477
  labelFor: () => labelFor,
@@ -8937,7 +9038,7 @@ var TERMINALHIRE_DIR4, KEY_FILE2, KEYTAR_SERVICE, KEYTAR_ACCOUNT, ALGO2, KEY_BYT
8937
9038
  var init_crypto_store = __esm({
8938
9039
  "src/crypto-store.ts"() {
8939
9040
  "use strict";
8940
- TERMINALHIRE_DIR4 = join5(homedir4(), ".terminalhire");
9041
+ TERMINALHIRE_DIR4 = process.env.TERMINALHIRE_DIR || join5(homedir4(), ".terminalhire");
8941
9042
  KEY_FILE2 = join5(TERMINALHIRE_DIR4, "key");
8942
9043
  KEYTAR_SERVICE = "terminalhire";
8943
9044
  KEYTAR_ACCOUNT = "profile-key";
@@ -9087,7 +9188,7 @@ var init_profile = __esm({
9087
9188
  "use strict";
9088
9189
  init_src();
9089
9190
  init_crypto_store();
9090
- TERMINALHIRE_DIR5 = join6(homedir5(), ".terminalhire");
9191
+ TERMINALHIRE_DIR5 = process.env.TERMINALHIRE_DIR || join6(homedir5(), ".terminalhire");
9091
9192
  PROFILE_FILE = join6(TERMINALHIRE_DIR5, "profile.enc");
9092
9193
  profileStore = createEncryptedStore(PROFILE_FILE, {
9093
9194
  blank: blankProfile,
@@ -9726,7 +9827,7 @@ import {
9726
9827
  } from "fs";
9727
9828
  import { join as join3 } from "path";
9728
9829
  import { homedir as homedir2 } from "os";
9729
- var TERMINALHIRE_DIR2 = join3(homedir2(), ".terminalhire");
9830
+ var TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join3(homedir2(), ".terminalhire");
9730
9831
  var TOKEN_FILE = join3(TERMINALHIRE_DIR2, "github-token.enc");
9731
9832
  var KEY_FILE = join3(TERMINALHIRE_DIR2, "key");
9732
9833
  var ALGO = "aes-256-gcm";
@@ -8,7 +8,7 @@ import { homedir as homedir2 } from "os";
8
8
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
9
9
  import { join } from "path";
10
10
  import { homedir } from "os";
11
- var TERMINALHIRE_DIR = join(homedir(), ".terminalhire");
11
+ var TERMINALHIRE_DIR = process.env.TERMINALHIRE_DIR || join(homedir(), ".terminalhire");
12
12
  var CONFIG_FILE = join(TERMINALHIRE_DIR, "config.json");
13
13
  var DEFAULT_CONFIG = {
14
14
  nudge: "session",
@@ -19,8 +19,7 @@ var DEFAULT_CONFIG = {
19
19
  chatShareActivity: false,
20
20
  inboundNudgeMuted: false,
21
21
  inboundNudgeDisclosed: false,
22
- contributeEnabled: false,
23
- contributePrompted: false,
22
+ contributeEnabled: true,
24
23
  betaOptIn: false,
25
24
  lastFullFeedbackAt: null,
26
25
  lastPulseAskAt: null,
@@ -41,6 +40,12 @@ function writeConfig(config) {
41
40
  mkdirSync(TERMINALHIRE_DIR, { recursive: true });
42
41
  const current = readConfig();
43
42
  const merged = { ...current, ...config };
43
+ if ("contributePrompted" in merged) {
44
+ if (merged.contributeEnabled === false && !("contributeEnabled" in config)) {
45
+ delete merged.contributeEnabled;
46
+ }
47
+ delete merged.contributePrompted;
48
+ }
44
49
  writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
45
50
  }
46
51
  function parseSurfaceMix(raw) {
@@ -1280,7 +1280,11 @@ var init_contributions = __esm({
1280
1280
  'label:"good-first-issue" type:issue state:open',
1281
1281
  'label:"help wanted" type:issue state:open',
1282
1282
  'label:"help-wanted" type:issue state:open',
1283
- 'label:"up-for-grabs" type:issue state:open'
1283
+ 'label:"up-for-grabs" type:issue state:open',
1284
+ // supply-expansion D: two more first-contribution label families widen the
1285
+ // global newest-first slice WITHOUT relaxing the credential gate.
1286
+ 'label:"beginner-friendly" type:issue state:open',
1287
+ 'label:"first-timers-only" type:issue state:open'
1284
1288
  ];
1285
1289
  CONTRIB_LANGUAGE_QUERIES = [
1286
1290
  ...["rust", "go", "python", "c++", "ruby"].map(
@@ -1288,6 +1292,17 @@ var init_contributions = __esm({
1288
1292
  ),
1289
1293
  ...["rust", "go"].map(
1290
1294
  (lang) => `label:"good first issue" language:${lang} type:issue state:open`
1295
+ ),
1296
+ // supply-expansion D: cover the high-volume web/enterprise ecosystems the
1297
+ // original set omitted. TS/JS were previously left out of "good first issue"
1298
+ // (the global slice over-represented them) but a LANGUAGE-scoped page surfaces
1299
+ // DIFFERENT repos than the global newest-first slice, so re-including them widens
1300
+ // distinct-repo coverage rather than duplicating it.
1301
+ ...["typescript", "javascript", "java", "python"].map(
1302
+ (lang) => `label:"good first issue" language:${lang} type:issue state:open`
1303
+ ),
1304
+ ...["typescript", "javascript", "c#", "php"].map(
1305
+ (lang) => `label:"help wanted" language:${lang} type:issue state:open`
1291
1306
  )
1292
1307
  ];
1293
1308
  CONTRIB_SEARCH_QUERIES = [...CONTRIB_LABEL_QUERIES, ...CONTRIB_LANGUAGE_QUERIES];
@@ -1633,7 +1648,7 @@ var TERMINALHIRE_DIR3, KEY_FILE, KEYTAR_SERVICE, KEYTAR_ACCOUNT, ALGO, KEY_BYTES
1633
1648
  var init_crypto_store = __esm({
1634
1649
  "src/crypto-store.ts"() {
1635
1650
  "use strict";
1636
- TERMINALHIRE_DIR3 = join4(homedir3(), ".terminalhire");
1651
+ TERMINALHIRE_DIR3 = process.env.TERMINALHIRE_DIR || join4(homedir3(), ".terminalhire");
1637
1652
  KEY_FILE = join4(TERMINALHIRE_DIR3, "key");
1638
1653
  KEYTAR_SERVICE = "terminalhire";
1639
1654
  KEYTAR_ACCOUNT = "profile-key";
@@ -1783,7 +1798,7 @@ var init_profile = __esm({
1783
1798
  "use strict";
1784
1799
  init_src();
1785
1800
  init_crypto_store();
1786
- TERMINALHIRE_DIR4 = join5(homedir4(), ".terminalhire");
1801
+ TERMINALHIRE_DIR4 = process.env.TERMINALHIRE_DIR || join5(homedir4(), ".terminalhire");
1787
1802
  PROFILE_FILE = join5(TERMINALHIRE_DIR4, "profile.enc");
1788
1803
  profileStore = createEncryptedStore(PROFILE_FILE, {
1789
1804
  blank: blankProfile,
@@ -2239,7 +2254,6 @@ init_src();
2239
2254
  import { readFileSync as readFileSync6 } from "fs";
2240
2255
  import { join as join8 } from "path";
2241
2256
  import { homedir as homedir7 } from "os";
2242
- import { createInterface } from "readline";
2243
2257
 
2244
2258
  // bin/cache-store.js
2245
2259
  import { readFileSync as readFileSync2, writeFileSync, mkdirSync, renameSync } from "fs";
@@ -2275,7 +2289,7 @@ function updateIndexCache(patch) {
2275
2289
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync } from "fs";
2276
2290
  import { join as join3 } from "path";
2277
2291
  import { homedir as homedir2 } from "os";
2278
- var TERMINALHIRE_DIR2 = join3(homedir2(), ".terminalhire");
2292
+ var TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join3(homedir2(), ".terminalhire");
2279
2293
  var CONFIG_FILE = join3(TERMINALHIRE_DIR2, "config.json");
2280
2294
  var DEFAULT_CONFIG = {
2281
2295
  nudge: "session",
@@ -2286,8 +2300,7 @@ var DEFAULT_CONFIG = {
2286
2300
  chatShareActivity: false,
2287
2301
  inboundNudgeMuted: false,
2288
2302
  inboundNudgeDisclosed: false,
2289
- contributeEnabled: false,
2290
- contributePrompted: false,
2303
+ contributeEnabled: true,
2291
2304
  betaOptIn: false,
2292
2305
  lastFullFeedbackAt: null,
2293
2306
  lastPulseAskAt: null,
@@ -2304,14 +2317,9 @@ function readConfig() {
2304
2317
  return { ...DEFAULT_CONFIG };
2305
2318
  }
2306
2319
  }
2307
- function writeConfig(config) {
2308
- mkdirSync2(TERMINALHIRE_DIR2, { recursive: true });
2309
- const current = readConfig();
2310
- const merged = { ...current, ...config };
2311
- writeFileSync2(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
2312
- }
2313
2320
  function isContributeEnabled() {
2314
- return readConfig().contributeEnabled === true;
2321
+ const cfg = readConfig();
2322
+ return !(cfg.contributeEnabled === false && !("contributePrompted" in cfg));
2315
2323
  }
2316
2324
 
2317
2325
  // bin/sanitize.js
@@ -2352,7 +2360,7 @@ var INDEX_TTL_MS = 15 * 60 * 1e3;
2352
2360
  var API_URL = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
2353
2361
  var CONTINUITY_RANK_DISABLED = process.env["TERMINALHIRE_NO_CONTINUITY_RANK"] === "1";
2354
2362
  var HEADER = "Contribution opportunities \u2014 open issues where a merged PR actually counts toward your r\xE9sum\xE9.\nRepos \u226550\u2605 / \u226510 contributors \xB7 unassigned \xB7 merit-merge \xB7 matched to your stack.";
2355
- var OPT_IN_PROMPT = "Contribute is off. Turn it on to see open issues where a merged PR would count toward your r\xE9sum\xE9 \u2014\nmatched to your stack. Your profile never leaves this machine. Enable? [y/N]";
2363
+ var CONTRIBUTE_OFF = "Contribute is off \u2014 you set contributeEnabled: false in ~/.terminalhire/config.json.\nRemove that line (or set it to true) to see open issues where a merged PR would count toward your r\xE9sum\xE9.";
2356
2364
  var EMPTY_STATE = "Nothing clears the bar right now. We only list issues where a merged PR actually counts toward\nyour r\xE9sum\xE9 \u2014 so the list stays honest. Try again after the next refresh.";
2357
2365
  function readIndexCache() {
2358
2366
  try {
@@ -2377,15 +2385,6 @@ async function fetchIndex(fetchImpl, useCache = true) {
2377
2385
  if (useCache) writeIndexCache(index);
2378
2386
  return index;
2379
2387
  }
2380
- function defaultPrompt(question) {
2381
- const rl = createInterface({ input: process.stdin, output: process.stdout });
2382
- return new Promise((resolve) => {
2383
- rl.question(question, (answer) => {
2384
- rl.close();
2385
- resolve(answer.trim().toLowerCase());
2386
- });
2387
- });
2388
- }
2389
2388
  var STRONG_THRESHOLD = 0.5;
2390
2389
  if (STRONG_THRESHOLD <= 0) {
2391
2390
  throw new Error("STRONG_THRESHOLD must be > 0 or the contested-issue fail-safe breaks");
@@ -2496,22 +2495,12 @@ ${line3}`;
2496
2495
  }
2497
2496
  async function run(opts = {}) {
2498
2497
  const log = opts.log ?? console.log;
2499
- const isTTY = opts.isTTY ?? Boolean(process.stdin.isTTY);
2500
- const promptImpl = opts.prompt ?? defaultPrompt;
2501
2498
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
2502
2499
  const useCache = opts.fetchImpl == null;
2503
2500
  try {
2504
2501
  if (!isContributeEnabled()) {
2505
- if (isTTY) {
2506
- const answer = await promptImpl(OPT_IN_PROMPT + " ");
2507
- const yes = answer === "y" || answer === "yes";
2508
- writeConfig(yes ? { contributeEnabled: true, contributePrompted: true } : { contributePrompted: true });
2509
- if (!yes) return;
2510
- } else {
2511
- log(OPT_IN_PROMPT);
2512
- writeConfig({ contributePrompted: true });
2513
- return;
2514
- }
2502
+ log(CONTRIBUTE_OFF);
2503
+ return;
2515
2504
  }
2516
2505
  const index = await fetchIndex(fetchImpl, useCache);
2517
2506
  const items = Array.isArray(index?.contribute) ? index.contribute : [];