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
@@ -253,7 +253,7 @@ import {
253
253
  } from "fs";
254
254
  import { join } from "path";
255
255
  import { homedir } from "os";
256
- var TERMINALHIRE_DIR = join(homedir(), ".terminalhire");
256
+ var TERMINALHIRE_DIR = process.env.TERMINALHIRE_DIR || join(homedir(), ".terminalhire");
257
257
  var TOKEN_FILE = join(TERMINALHIRE_DIR, "github-token.enc");
258
258
  var KEY_FILE = join(TERMINALHIRE_DIR, "key");
259
259
  var ALGO = "aes-256-gcm";
@@ -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,
@@ -8598,7 +8699,7 @@ var TERMINALHIRE_DIR2, KEY_FILE, KEYTAR_SERVICE, KEYTAR_ACCOUNT, ALGO, KEY_BYTES
8598
8699
  var init_crypto_store = __esm({
8599
8700
  "src/crypto-store.ts"() {
8600
8701
  "use strict";
8601
- TERMINALHIRE_DIR2 = join3(homedir2(), ".terminalhire");
8702
+ TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join3(homedir2(), ".terminalhire");
8602
8703
  KEY_FILE = join3(TERMINALHIRE_DIR2, "key");
8603
8704
  KEYTAR_SERVICE = "terminalhire";
8604
8705
  KEYTAR_ACCOUNT = "profile-key";
@@ -8748,7 +8849,7 @@ var init_profile = __esm({
8748
8849
  "use strict";
8749
8850
  init_src();
8750
8851
  init_crypto_store();
8751
- TERMINALHIRE_DIR3 = join4(homedir3(), ".terminalhire");
8852
+ TERMINALHIRE_DIR3 = process.env.TERMINALHIRE_DIR || join4(homedir3(), ".terminalhire");
8752
8853
  PROFILE_FILE = join4(TERMINALHIRE_DIR3, "profile.enc");
8753
8854
  profileStore = createEncryptedStore(PROFILE_FILE, {
8754
8855
  blank: blankProfile,
@@ -892,7 +892,11 @@ var init_contributions = __esm({
892
892
  'label:"good-first-issue" type:issue state:open',
893
893
  'label:"help wanted" type:issue state:open',
894
894
  'label:"help-wanted" type:issue state:open',
895
- 'label:"up-for-grabs" type:issue state:open'
895
+ 'label:"up-for-grabs" type:issue state:open',
896
+ // supply-expansion D: two more first-contribution label families widen the
897
+ // global newest-first slice WITHOUT relaxing the credential gate.
898
+ 'label:"beginner-friendly" type:issue state:open',
899
+ 'label:"first-timers-only" type:issue state:open'
896
900
  ];
897
901
  CONTRIB_LANGUAGE_QUERIES = [
898
902
  ...["rust", "go", "python", "c++", "ruby"].map(
@@ -900,6 +904,17 @@ var init_contributions = __esm({
900
904
  ),
901
905
  ...["rust", "go"].map(
902
906
  (lang) => `label:"good first issue" language:${lang} type:issue state:open`
907
+ ),
908
+ // supply-expansion D: cover the high-volume web/enterprise ecosystems the
909
+ // original set omitted. TS/JS were previously left out of "good first issue"
910
+ // (the global slice over-represented them) but a LANGUAGE-scoped page surfaces
911
+ // DIFFERENT repos than the global newest-first slice, so re-including them widens
912
+ // distinct-repo coverage rather than duplicating it.
913
+ ...["typescript", "javascript", "java", "python"].map(
914
+ (lang) => `label:"good first issue" language:${lang} type:issue state:open`
915
+ ),
916
+ ...["typescript", "javascript", "c#", "php"].map(
917
+ (lang) => `label:"help wanted" language:${lang} type:issue state:open`
903
918
  )
904
919
  ];
905
920
  CONTRIB_SEARCH_QUERIES = [...CONTRIB_LABEL_QUERIES, ...CONTRIB_LANGUAGE_QUERIES];
@@ -4183,7 +4198,7 @@ var TERMINALHIRE_DIR, TOKEN_FILE, KEY_FILE, ALGO, KEY_BYTES, IV_BYTES;
4183
4198
  var init_github_auth = __esm({
4184
4199
  "src/github-auth.ts"() {
4185
4200
  "use strict";
4186
- TERMINALHIRE_DIR = join2(homedir(), ".terminalhire");
4201
+ TERMINALHIRE_DIR = process.env.TERMINALHIRE_DIR || join2(homedir(), ".terminalhire");
4187
4202
  TOKEN_FILE = join2(TERMINALHIRE_DIR, "github-token.enc");
4188
4203
  KEY_FILE = join2(TERMINALHIRE_DIR, "key");
4189
4204
  ALGO = "aes-256-gcm";
@@ -4214,7 +4229,7 @@ var init_chat_keystore = __esm({
4214
4229
  "use strict";
4215
4230
  init_src();
4216
4231
  init_github_auth();
4217
- TERMINALHIRE_DIR2 = join3(homedir2(), ".terminalhire");
4232
+ TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join3(homedir2(), ".terminalhire");
4218
4233
  IDENTITY_FILE = join3(TERMINALHIRE_DIR2, "chat-identity.enc");
4219
4234
  }
4220
4235
  });
@@ -4231,7 +4246,7 @@ import {
4231
4246
  import { homedir as homedir3 } from "os";
4232
4247
  import { join as join4 } from "path";
4233
4248
  function terminalhireDir() {
4234
- return join4(homedir3(), ".terminalhire");
4249
+ return process.env.TERMINALHIRE_DIR || join4(homedir3(), ".terminalhire");
4235
4250
  }
4236
4251
  function webSessionFilePath() {
4237
4252
  return join4(terminalhireDir(), "web-session");
@@ -4456,7 +4471,7 @@ var init_chat_client = __esm({
4456
4471
  init_web_session();
4457
4472
  CHAT_BASE = process.env["TERMINALHIRE_API_URL"] || "https://terminalhire.com";
4458
4473
  GH_SESSION_COOKIE = "__jpi_gh_session";
4459
- TERMINALHIRE_DIR3 = join5(homedir4(), ".terminalhire");
4474
+ TERMINALHIRE_DIR3 = process.env.TERMINALHIRE_DIR || join5(homedir4(), ".terminalhire");
4460
4475
  PEERS_FILE = join5(TERMINALHIRE_DIR3, "chat-peers.json");
4461
4476
  REQUEST_TIMEOUT_MS = 1e4;
4462
4477
  ChatNotLinkedError = class extends Error {
@@ -4516,13 +4531,19 @@ function writeConfig(config) {
4516
4531
  mkdirSync5(TERMINALHIRE_DIR4, { recursive: true });
4517
4532
  const current = readConfig();
4518
4533
  const merged = { ...current, ...config };
4534
+ if ("contributePrompted" in merged) {
4535
+ if (merged.contributeEnabled === false && !("contributeEnabled" in config)) {
4536
+ delete merged.contributeEnabled;
4537
+ }
4538
+ delete merged.contributePrompted;
4539
+ }
4519
4540
  writeFileSync5(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
4520
4541
  }
4521
4542
  var TERMINALHIRE_DIR4, CONFIG_FILE, DEFAULT_CONFIG;
4522
4543
  var init_config = __esm({
4523
4544
  "src/config.ts"() {
4524
4545
  "use strict";
4525
- TERMINALHIRE_DIR4 = join6(homedir5(), ".terminalhire");
4546
+ TERMINALHIRE_DIR4 = process.env.TERMINALHIRE_DIR || join6(homedir5(), ".terminalhire");
4526
4547
  CONFIG_FILE = join6(TERMINALHIRE_DIR4, "config.json");
4527
4548
  DEFAULT_CONFIG = {
4528
4549
  nudge: "session",
@@ -4533,8 +4554,7 @@ var init_config = __esm({
4533
4554
  chatShareActivity: false,
4534
4555
  inboundNudgeMuted: false,
4535
4556
  inboundNudgeDisclosed: false,
4536
- contributeEnabled: false,
4537
- contributePrompted: false,
4557
+ contributeEnabled: true,
4538
4558
  betaOptIn: false,
4539
4559
  lastFullFeedbackAt: null,
4540
4560
  lastPulseAskAt: null,
@@ -909,7 +909,11 @@ var init_contributions = __esm({
909
909
  'label:"good-first-issue" type:issue state:open',
910
910
  'label:"help wanted" type:issue state:open',
911
911
  'label:"help-wanted" type:issue state:open',
912
- 'label:"up-for-grabs" type:issue state:open'
912
+ 'label:"up-for-grabs" type:issue state:open',
913
+ // supply-expansion D: two more first-contribution label families widen the
914
+ // global newest-first slice WITHOUT relaxing the credential gate.
915
+ 'label:"beginner-friendly" type:issue state:open',
916
+ 'label:"first-timers-only" type:issue state:open'
913
917
  ];
914
918
  CONTRIB_LANGUAGE_QUERIES = [
915
919
  ...["rust", "go", "python", "c++", "ruby"].map(
@@ -917,6 +921,17 @@ var init_contributions = __esm({
917
921
  ),
918
922
  ...["rust", "go"].map(
919
923
  (lang) => `label:"good first issue" language:${lang} type:issue state:open`
924
+ ),
925
+ // supply-expansion D: cover the high-volume web/enterprise ecosystems the
926
+ // original set omitted. TS/JS were previously left out of "good first issue"
927
+ // (the global slice over-represented them) but a LANGUAGE-scoped page surfaces
928
+ // DIFFERENT repos than the global newest-first slice, so re-including them widens
929
+ // distinct-repo coverage rather than duplicating it.
930
+ ...["typescript", "javascript", "java", "python"].map(
931
+ (lang) => `label:"good first issue" language:${lang} type:issue state:open`
932
+ ),
933
+ ...["typescript", "javascript", "c#", "php"].map(
934
+ (lang) => `label:"help wanted" language:${lang} type:issue state:open`
920
935
  )
921
936
  ];
922
937
  CONTRIB_SEARCH_QUERIES = [...CONTRIB_LABEL_QUERIES, ...CONTRIB_LANGUAGE_QUERIES];
@@ -4211,7 +4226,7 @@ var TERMINALHIRE_DIR, TOKEN_FILE, KEY_FILE, ALGO, KEY_BYTES, IV_BYTES;
4211
4226
  var init_github_auth = __esm({
4212
4227
  "src/github-auth.ts"() {
4213
4228
  "use strict";
4214
- TERMINALHIRE_DIR = join2(homedir(), ".terminalhire");
4229
+ TERMINALHIRE_DIR = process.env.TERMINALHIRE_DIR || join2(homedir(), ".terminalhire");
4215
4230
  TOKEN_FILE = join2(TERMINALHIRE_DIR, "github-token.enc");
4216
4231
  KEY_FILE = join2(TERMINALHIRE_DIR, "key");
4217
4232
  ALGO = "aes-256-gcm";
@@ -4242,7 +4257,7 @@ var init_chat_keystore = __esm({
4242
4257
  "use strict";
4243
4258
  init_src();
4244
4259
  init_github_auth();
4245
- TERMINALHIRE_DIR2 = join3(homedir2(), ".terminalhire");
4260
+ TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join3(homedir2(), ".terminalhire");
4246
4261
  IDENTITY_FILE = join3(TERMINALHIRE_DIR2, "chat-identity.enc");
4247
4262
  }
4248
4263
  });
@@ -4259,7 +4274,7 @@ import {
4259
4274
  import { homedir as homedir3 } from "os";
4260
4275
  import { join as join4 } from "path";
4261
4276
  function terminalhireDir() {
4262
- return join4(homedir3(), ".terminalhire");
4277
+ return process.env.TERMINALHIRE_DIR || join4(homedir3(), ".terminalhire");
4263
4278
  }
4264
4279
  function webSessionFilePath() {
4265
4280
  return join4(terminalhireDir(), "web-session");
@@ -4484,7 +4499,7 @@ var init_chat_client = __esm({
4484
4499
  init_web_session();
4485
4500
  CHAT_BASE = process.env["TERMINALHIRE_API_URL"] || "https://terminalhire.com";
4486
4501
  GH_SESSION_COOKIE = "__jpi_gh_session";
4487
- TERMINALHIRE_DIR3 = join5(homedir4(), ".terminalhire");
4502
+ TERMINALHIRE_DIR3 = process.env.TERMINALHIRE_DIR || join5(homedir4(), ".terminalhire");
4488
4503
  PEERS_FILE = join5(TERMINALHIRE_DIR3, "chat-peers.json");
4489
4504
  REQUEST_TIMEOUT_MS = 1e4;
4490
4505
  ChatNotLinkedError = class extends Error {
@@ -4544,13 +4559,19 @@ function writeConfig(config) {
4544
4559
  mkdirSync5(TERMINALHIRE_DIR4, { recursive: true });
4545
4560
  const current = readConfig();
4546
4561
  const merged = { ...current, ...config };
4562
+ if ("contributePrompted" in merged) {
4563
+ if (merged.contributeEnabled === false && !("contributeEnabled" in config)) {
4564
+ delete merged.contributeEnabled;
4565
+ }
4566
+ delete merged.contributePrompted;
4567
+ }
4547
4568
  writeFileSync5(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
4548
4569
  }
4549
4570
  var TERMINALHIRE_DIR4, CONFIG_FILE, DEFAULT_CONFIG;
4550
4571
  var init_config = __esm({
4551
4572
  "src/config.ts"() {
4552
4573
  "use strict";
4553
- TERMINALHIRE_DIR4 = join6(homedir5(), ".terminalhire");
4574
+ TERMINALHIRE_DIR4 = process.env.TERMINALHIRE_DIR || join6(homedir5(), ".terminalhire");
4554
4575
  CONFIG_FILE = join6(TERMINALHIRE_DIR4, "config.json");
4555
4576
  DEFAULT_CONFIG = {
4556
4577
  nudge: "session",
@@ -4561,8 +4582,7 @@ var init_config = __esm({
4561
4582
  chatShareActivity: false,
4562
4583
  inboundNudgeMuted: false,
4563
4584
  inboundNudgeDisclosed: false,
4564
- contributeEnabled: false,
4565
- contributePrompted: false,
4585
+ contributeEnabled: true,
4566
4586
  betaOptIn: false,
4567
4587
  lastFullFeedbackAt: null,
4568
4588
  lastPulseAskAt: null,
@@ -5165,7 +5185,7 @@ var TERMINALHIRE_DIR6, KEY_FILE2, KEYTAR_SERVICE, KEYTAR_ACCOUNT, ALGO2, KEY_BYT
5165
5185
  var init_crypto_store = __esm({
5166
5186
  "src/crypto-store.ts"() {
5167
5187
  "use strict";
5168
- TERMINALHIRE_DIR6 = join8(homedir7(), ".terminalhire");
5188
+ TERMINALHIRE_DIR6 = process.env.TERMINALHIRE_DIR || join8(homedir7(), ".terminalhire");
5169
5189
  KEY_FILE2 = join8(TERMINALHIRE_DIR6, "key");
5170
5190
  KEYTAR_SERVICE = "terminalhire";
5171
5191
  KEYTAR_ACCOUNT = "profile-key";
@@ -5315,7 +5335,7 @@ var init_profile = __esm({
5315
5335
  "use strict";
5316
5336
  init_src();
5317
5337
  init_crypto_store();
5318
- TERMINALHIRE_DIR7 = join9(homedir8(), ".terminalhire");
5338
+ TERMINALHIRE_DIR7 = process.env.TERMINALHIRE_DIR || join9(homedir8(), ".terminalhire");
5319
5339
  PROFILE_FILE = join9(TERMINALHIRE_DIR7, "profile.enc");
5320
5340
  profileStore = createEncryptedStore(PROFILE_FILE, {
5321
5341
  blank: blankProfile,