terminalhire 0.35.3 → 0.35.4

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 (41) hide show
  1. package/dist/bin/claim-push-bg.js +1 -1
  2. package/dist/bin/jpi-bounties.js +20 -4
  3. package/dist/bin/jpi-chat-read.js +12 -7
  4. package/dist/bin/jpi-chat.js +14 -9
  5. package/dist/bin/jpi-claim.js +21 -5
  6. package/dist/bin/jpi-config.js +8 -3
  7. package/dist/bin/jpi-contribute.js +9 -35
  8. package/dist/bin/jpi-devs.js +21 -4
  9. package/dist/bin/jpi-dispatch.js +6145 -459
  10. package/dist/bin/jpi-hub.js +27 -11
  11. package/dist/bin/jpi-inbox.js +14 -9
  12. package/dist/bin/jpi-init.js +18 -2
  13. package/dist/bin/jpi-intro.js +3 -3
  14. package/dist/bin/jpi-jobs.js +29 -8
  15. package/dist/bin/jpi-learn.js +2 -2
  16. package/dist/bin/jpi-link.js +9 -4
  17. package/dist/bin/jpi-login.js +29 -8
  18. package/dist/bin/jpi-mcp.js +5838 -141
  19. package/dist/bin/jpi-profile.js +2 -2
  20. package/dist/bin/jpi-project.js +18 -2
  21. package/dist/bin/jpi-refresh.js +59 -46
  22. package/dist/bin/jpi-repo.js +2 -2
  23. package/dist/bin/jpi-save.js +2 -2
  24. package/dist/bin/jpi-spinner.js +0 -12
  25. package/dist/bin/jpi-sync.js +2 -2
  26. package/dist/bin/jpi-trajectory.js +4 -4
  27. package/dist/bin/peer-connect-prompt.js +8 -3
  28. package/dist/bin/pulse-prompt.js +9 -4
  29. package/dist/bin/spinner.js +0 -14
  30. package/dist/src/chat-client.js +4 -4
  31. package/dist/src/chat-keystore.js +2 -2
  32. package/dist/src/config.js +10 -8
  33. package/dist/src/crypto-store.js +1 -1
  34. package/dist/src/github-auth.js +1 -1
  35. package/dist/src/intro.js +3 -3
  36. package/dist/src/link.js +9 -4
  37. package/dist/src/profile.js +2 -2
  38. package/dist/src/repo-experience.js +2 -2
  39. package/dist/src/trajectory.js +4 -4
  40. package/dist/src/web-session.js +1 -1
  41. 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";
@@ -4054,7 +4054,12 @@ function buildContributionJob(a) {
4054
4054
  // TERM-27: persist the repo's primary language so project curation can
4055
4055
  // exclude the repo's OWN language id (folded into every issue's tags) from
4056
4056
  // the distinct-skill signal. Same `repo` used by the tokenize() tag build.
4057
- language: a.repo.language ?? null
4057
+ language: a.repo.language ?? null,
4058
+ // TERM-35: stamp the search item's comment count (already in the response —
4059
+ // zero extra egress). A NEUTRAL volume signal only: set solely when the
4060
+ // search item carries a finite count >= 0; a failed/absent value leaves it
4061
+ // undefined (never a fabricated 0), so a render's chip falls through cleanly.
4062
+ commentsAtDiscovery: typeof a.issue.comments === "number" && Number.isFinite(a.issue.comments) && a.issue.comments >= 0 ? a.issue.comments : void 0
4058
4063
  },
4059
4064
  // Provenance: repo-first discovered items only (label-first omits the field).
4060
4065
  ...a.discovered ? { discovered: true } : {},
@@ -4570,6 +4575,15 @@ async function enrichWinnability(jobs, contribute, w) {
4570
4575
  );
4571
4576
  }
4572
4577
  }
4578
+ function hasClickableUrl(url) {
4579
+ if (!url) return false;
4580
+ try {
4581
+ const parsed = new URL(url);
4582
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
4583
+ } catch {
4584
+ return false;
4585
+ }
4586
+ }
4573
4587
  async function buildIndex(opts) {
4574
4588
  const includePartners = opts?.includePartners ?? true;
4575
4589
  const publicJobs = await aggregate(opts);
@@ -4580,6 +4594,7 @@ async function buildIndex(opts) {
4580
4594
  ...opts?.partnerRoles ?? []
4581
4595
  ];
4582
4596
  for (const job of partnerJobs) {
4597
+ if (!hasClickableUrl(job.url)) continue;
4583
4598
  if (!seen.has(job.id)) {
4584
4599
  seen.add(job.id);
4585
4600
  allJobs.push(job);
@@ -4661,7 +4676,8 @@ async function fetchIssueStatus(fullName, issueNumber, opts = {}) {
4661
4676
  for (const a of body.assignees ?? []) {
4662
4677
  if (a && typeof a.login === "string") assignees.add(a.login);
4663
4678
  }
4664
- return { state, assignees: [...assignees] };
4679
+ const comments = typeof body.comments === "number" && Number.isFinite(body.comments) && body.comments >= 0 ? body.comments : null;
4680
+ return { state, assignees: [...assignees], comments };
4665
4681
  }
4666
4682
  var GITHUB_API3, DEFAULT_ISSUE_STATUS_TIMEOUT_MS;
4667
4683
  var init_github_issue_status = __esm({
@@ -8598,7 +8614,7 @@ var TERMINALHIRE_DIR2, KEY_FILE, KEYTAR_SERVICE, KEYTAR_ACCOUNT, ALGO, KEY_BYTES
8598
8614
  var init_crypto_store = __esm({
8599
8615
  "src/crypto-store.ts"() {
8600
8616
  "use strict";
8601
- TERMINALHIRE_DIR2 = join3(homedir2(), ".terminalhire");
8617
+ TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join3(homedir2(), ".terminalhire");
8602
8618
  KEY_FILE = join3(TERMINALHIRE_DIR2, "key");
8603
8619
  KEYTAR_SERVICE = "terminalhire";
8604
8620
  KEYTAR_ACCOUNT = "profile-key";
@@ -8748,7 +8764,7 @@ var init_profile = __esm({
8748
8764
  "use strict";
8749
8765
  init_src();
8750
8766
  init_crypto_store();
8751
- TERMINALHIRE_DIR3 = join4(homedir3(), ".terminalhire");
8767
+ TERMINALHIRE_DIR3 = process.env.TERMINALHIRE_DIR || join4(homedir3(), ".terminalhire");
8752
8768
  PROFILE_FILE = join4(TERMINALHIRE_DIR3, "profile.enc");
8753
8769
  profileStore = createEncryptedStore(PROFILE_FILE, {
8754
8770
  blank: blankProfile,
@@ -4183,7 +4183,7 @@ var TERMINALHIRE_DIR, TOKEN_FILE, KEY_FILE, ALGO, KEY_BYTES, IV_BYTES;
4183
4183
  var init_github_auth = __esm({
4184
4184
  "src/github-auth.ts"() {
4185
4185
  "use strict";
4186
- TERMINALHIRE_DIR = join2(homedir(), ".terminalhire");
4186
+ TERMINALHIRE_DIR = process.env.TERMINALHIRE_DIR || join2(homedir(), ".terminalhire");
4187
4187
  TOKEN_FILE = join2(TERMINALHIRE_DIR, "github-token.enc");
4188
4188
  KEY_FILE = join2(TERMINALHIRE_DIR, "key");
4189
4189
  ALGO = "aes-256-gcm";
@@ -4214,7 +4214,7 @@ var init_chat_keystore = __esm({
4214
4214
  "use strict";
4215
4215
  init_src();
4216
4216
  init_github_auth();
4217
- TERMINALHIRE_DIR2 = join3(homedir2(), ".terminalhire");
4217
+ TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join3(homedir2(), ".terminalhire");
4218
4218
  IDENTITY_FILE = join3(TERMINALHIRE_DIR2, "chat-identity.enc");
4219
4219
  }
4220
4220
  });
@@ -4231,7 +4231,7 @@ import {
4231
4231
  import { homedir as homedir3 } from "os";
4232
4232
  import { join as join4 } from "path";
4233
4233
  function terminalhireDir() {
4234
- return join4(homedir3(), ".terminalhire");
4234
+ return process.env.TERMINALHIRE_DIR || join4(homedir3(), ".terminalhire");
4235
4235
  }
4236
4236
  function webSessionFilePath() {
4237
4237
  return join4(terminalhireDir(), "web-session");
@@ -4456,7 +4456,7 @@ var init_chat_client = __esm({
4456
4456
  init_web_session();
4457
4457
  CHAT_BASE = process.env["TERMINALHIRE_API_URL"] || "https://terminalhire.com";
4458
4458
  GH_SESSION_COOKIE = "__jpi_gh_session";
4459
- TERMINALHIRE_DIR3 = join5(homedir4(), ".terminalhire");
4459
+ TERMINALHIRE_DIR3 = process.env.TERMINALHIRE_DIR || join5(homedir4(), ".terminalhire");
4460
4460
  PEERS_FILE = join5(TERMINALHIRE_DIR3, "chat-peers.json");
4461
4461
  REQUEST_TIMEOUT_MS = 1e4;
4462
4462
  ChatNotLinkedError = class extends Error {
@@ -4516,13 +4516,19 @@ function writeConfig(config) {
4516
4516
  mkdirSync5(TERMINALHIRE_DIR4, { recursive: true });
4517
4517
  const current = readConfig();
4518
4518
  const merged = { ...current, ...config };
4519
+ if ("contributePrompted" in merged) {
4520
+ if (merged.contributeEnabled === false && !("contributeEnabled" in config)) {
4521
+ delete merged.contributeEnabled;
4522
+ }
4523
+ delete merged.contributePrompted;
4524
+ }
4519
4525
  writeFileSync5(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
4520
4526
  }
4521
4527
  var TERMINALHIRE_DIR4, CONFIG_FILE, DEFAULT_CONFIG;
4522
4528
  var init_config = __esm({
4523
4529
  "src/config.ts"() {
4524
4530
  "use strict";
4525
- TERMINALHIRE_DIR4 = join6(homedir5(), ".terminalhire");
4531
+ TERMINALHIRE_DIR4 = process.env.TERMINALHIRE_DIR || join6(homedir5(), ".terminalhire");
4526
4532
  CONFIG_FILE = join6(TERMINALHIRE_DIR4, "config.json");
4527
4533
  DEFAULT_CONFIG = {
4528
4534
  nudge: "session",
@@ -4533,8 +4539,7 @@ var init_config = __esm({
4533
4539
  chatShareActivity: false,
4534
4540
  inboundNudgeMuted: false,
4535
4541
  inboundNudgeDisclosed: false,
4536
- contributeEnabled: false,
4537
- contributePrompted: false,
4542
+ contributeEnabled: true,
4538
4543
  betaOptIn: false,
4539
4544
  lastFullFeedbackAt: null,
4540
4545
  lastPulseAskAt: null,
@@ -4211,7 +4211,7 @@ var TERMINALHIRE_DIR, TOKEN_FILE, KEY_FILE, ALGO, KEY_BYTES, IV_BYTES;
4211
4211
  var init_github_auth = __esm({
4212
4212
  "src/github-auth.ts"() {
4213
4213
  "use strict";
4214
- TERMINALHIRE_DIR = join2(homedir(), ".terminalhire");
4214
+ TERMINALHIRE_DIR = process.env.TERMINALHIRE_DIR || join2(homedir(), ".terminalhire");
4215
4215
  TOKEN_FILE = join2(TERMINALHIRE_DIR, "github-token.enc");
4216
4216
  KEY_FILE = join2(TERMINALHIRE_DIR, "key");
4217
4217
  ALGO = "aes-256-gcm";
@@ -4242,7 +4242,7 @@ var init_chat_keystore = __esm({
4242
4242
  "use strict";
4243
4243
  init_src();
4244
4244
  init_github_auth();
4245
- TERMINALHIRE_DIR2 = join3(homedir2(), ".terminalhire");
4245
+ TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join3(homedir2(), ".terminalhire");
4246
4246
  IDENTITY_FILE = join3(TERMINALHIRE_DIR2, "chat-identity.enc");
4247
4247
  }
4248
4248
  });
@@ -4259,7 +4259,7 @@ import {
4259
4259
  import { homedir as homedir3 } from "os";
4260
4260
  import { join as join4 } from "path";
4261
4261
  function terminalhireDir() {
4262
- return join4(homedir3(), ".terminalhire");
4262
+ return process.env.TERMINALHIRE_DIR || join4(homedir3(), ".terminalhire");
4263
4263
  }
4264
4264
  function webSessionFilePath() {
4265
4265
  return join4(terminalhireDir(), "web-session");
@@ -4484,7 +4484,7 @@ var init_chat_client = __esm({
4484
4484
  init_web_session();
4485
4485
  CHAT_BASE = process.env["TERMINALHIRE_API_URL"] || "https://terminalhire.com";
4486
4486
  GH_SESSION_COOKIE = "__jpi_gh_session";
4487
- TERMINALHIRE_DIR3 = join5(homedir4(), ".terminalhire");
4487
+ TERMINALHIRE_DIR3 = process.env.TERMINALHIRE_DIR || join5(homedir4(), ".terminalhire");
4488
4488
  PEERS_FILE = join5(TERMINALHIRE_DIR3, "chat-peers.json");
4489
4489
  REQUEST_TIMEOUT_MS = 1e4;
4490
4490
  ChatNotLinkedError = class extends Error {
@@ -4544,13 +4544,19 @@ function writeConfig(config) {
4544
4544
  mkdirSync5(TERMINALHIRE_DIR4, { recursive: true });
4545
4545
  const current = readConfig();
4546
4546
  const merged = { ...current, ...config };
4547
+ if ("contributePrompted" in merged) {
4548
+ if (merged.contributeEnabled === false && !("contributeEnabled" in config)) {
4549
+ delete merged.contributeEnabled;
4550
+ }
4551
+ delete merged.contributePrompted;
4552
+ }
4547
4553
  writeFileSync5(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
4548
4554
  }
4549
4555
  var TERMINALHIRE_DIR4, CONFIG_FILE, DEFAULT_CONFIG;
4550
4556
  var init_config = __esm({
4551
4557
  "src/config.ts"() {
4552
4558
  "use strict";
4553
- TERMINALHIRE_DIR4 = join6(homedir5(), ".terminalhire");
4559
+ TERMINALHIRE_DIR4 = process.env.TERMINALHIRE_DIR || join6(homedir5(), ".terminalhire");
4554
4560
  CONFIG_FILE = join6(TERMINALHIRE_DIR4, "config.json");
4555
4561
  DEFAULT_CONFIG = {
4556
4562
  nudge: "session",
@@ -4561,8 +4567,7 @@ var init_config = __esm({
4561
4567
  chatShareActivity: false,
4562
4568
  inboundNudgeMuted: false,
4563
4569
  inboundNudgeDisclosed: false,
4564
- contributeEnabled: false,
4565
- contributePrompted: false,
4570
+ contributeEnabled: true,
4566
4571
  betaOptIn: false,
4567
4572
  lastFullFeedbackAt: null,
4568
4573
  lastPulseAskAt: null,
@@ -5165,7 +5170,7 @@ var TERMINALHIRE_DIR6, KEY_FILE2, KEYTAR_SERVICE, KEYTAR_ACCOUNT, ALGO2, KEY_BYT
5165
5170
  var init_crypto_store = __esm({
5166
5171
  "src/crypto-store.ts"() {
5167
5172
  "use strict";
5168
- TERMINALHIRE_DIR6 = join8(homedir7(), ".terminalhire");
5173
+ TERMINALHIRE_DIR6 = process.env.TERMINALHIRE_DIR || join8(homedir7(), ".terminalhire");
5169
5174
  KEY_FILE2 = join8(TERMINALHIRE_DIR6, "key");
5170
5175
  KEYTAR_SERVICE = "terminalhire";
5171
5176
  KEYTAR_ACCOUNT = "profile-key";
@@ -5315,7 +5320,7 @@ var init_profile = __esm({
5315
5320
  "use strict";
5316
5321
  init_src();
5317
5322
  init_crypto_store();
5318
- TERMINALHIRE_DIR7 = join9(homedir8(), ".terminalhire");
5323
+ TERMINALHIRE_DIR7 = process.env.TERMINALHIRE_DIR || join9(homedir8(), ".terminalhire");
5319
5324
  PROFILE_FILE = join9(TERMINALHIRE_DIR7, "profile.enc");
5320
5325
  profileStore = createEncryptedStore(PROFILE_FILE, {
5321
5326
  blank: blankProfile,
@@ -4054,7 +4054,12 @@ function buildContributionJob(a) {
4054
4054
  // TERM-27: persist the repo's primary language so project curation can
4055
4055
  // exclude the repo's OWN language id (folded into every issue's tags) from
4056
4056
  // the distinct-skill signal. Same `repo` used by the tokenize() tag build.
4057
- language: a.repo.language ?? null
4057
+ language: a.repo.language ?? null,
4058
+ // TERM-35: stamp the search item's comment count (already in the response —
4059
+ // zero extra egress). A NEUTRAL volume signal only: set solely when the
4060
+ // search item carries a finite count >= 0; a failed/absent value leaves it
4061
+ // undefined (never a fabricated 0), so a render's chip falls through cleanly.
4062
+ commentsAtDiscovery: typeof a.issue.comments === "number" && Number.isFinite(a.issue.comments) && a.issue.comments >= 0 ? a.issue.comments : void 0
4058
4063
  },
4059
4064
  // Provenance: repo-first discovered items only (label-first omits the field).
4060
4065
  ...a.discovered ? { discovered: true } : {},
@@ -4570,6 +4575,15 @@ async function enrichWinnability(jobs, contribute, w) {
4570
4575
  );
4571
4576
  }
4572
4577
  }
4578
+ function hasClickableUrl(url) {
4579
+ if (!url) return false;
4580
+ try {
4581
+ const parsed = new URL(url);
4582
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
4583
+ } catch {
4584
+ return false;
4585
+ }
4586
+ }
4573
4587
  async function buildIndex(opts) {
4574
4588
  const includePartners = opts?.includePartners ?? true;
4575
4589
  const publicJobs = await aggregate(opts);
@@ -4580,6 +4594,7 @@ async function buildIndex(opts) {
4580
4594
  ...opts?.partnerRoles ?? []
4581
4595
  ];
4582
4596
  for (const job of partnerJobs) {
4597
+ if (!hasClickableUrl(job.url)) continue;
4583
4598
  if (!seen.has(job.id)) {
4584
4599
  seen.add(job.id);
4585
4600
  allJobs.push(job);
@@ -4661,7 +4676,8 @@ async function fetchIssueStatus(fullName, issueNumber, opts = {}) {
4661
4676
  for (const a of body.assignees ?? []) {
4662
4677
  if (a && typeof a.login === "string") assignees.add(a.login);
4663
4678
  }
4664
- return { state, assignees: [...assignees] };
4679
+ const comments = typeof body.comments === "number" && Number.isFinite(body.comments) && body.comments >= 0 ? body.comments : null;
4680
+ return { state, assignees: [...assignees], comments };
4665
4681
  }
4666
4682
  var GITHUB_API3, DEFAULT_ISSUE_STATUS_TIMEOUT_MS;
4667
4683
  var init_github_issue_status = __esm({
@@ -8937,7 +8953,7 @@ var TERMINALHIRE_DIR4, KEY_FILE2, KEYTAR_SERVICE, KEYTAR_ACCOUNT, ALGO2, KEY_BYT
8937
8953
  var init_crypto_store = __esm({
8938
8954
  "src/crypto-store.ts"() {
8939
8955
  "use strict";
8940
- TERMINALHIRE_DIR4 = join5(homedir4(), ".terminalhire");
8956
+ TERMINALHIRE_DIR4 = process.env.TERMINALHIRE_DIR || join5(homedir4(), ".terminalhire");
8941
8957
  KEY_FILE2 = join5(TERMINALHIRE_DIR4, "key");
8942
8958
  KEYTAR_SERVICE = "terminalhire";
8943
8959
  KEYTAR_ACCOUNT = "profile-key";
@@ -9087,7 +9103,7 @@ var init_profile = __esm({
9087
9103
  "use strict";
9088
9104
  init_src();
9089
9105
  init_crypto_store();
9090
- TERMINALHIRE_DIR5 = join6(homedir5(), ".terminalhire");
9106
+ TERMINALHIRE_DIR5 = process.env.TERMINALHIRE_DIR || join6(homedir5(), ".terminalhire");
9091
9107
  PROFILE_FILE = join6(TERMINALHIRE_DIR5, "profile.enc");
9092
9108
  profileStore = createEncryptedStore(PROFILE_FILE, {
9093
9109
  blank: blankProfile,
@@ -9726,7 +9742,7 @@ import {
9726
9742
  } from "fs";
9727
9743
  import { join as join3 } from "path";
9728
9744
  import { homedir as homedir2 } from "os";
9729
- var TERMINALHIRE_DIR2 = join3(homedir2(), ".terminalhire");
9745
+ var TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join3(homedir2(), ".terminalhire");
9730
9746
  var TOKEN_FILE = join3(TERMINALHIRE_DIR2, "github-token.enc");
9731
9747
  var KEY_FILE = join3(TERMINALHIRE_DIR2, "key");
9732
9748
  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) {
@@ -1633,7 +1633,7 @@ var TERMINALHIRE_DIR3, KEY_FILE, KEYTAR_SERVICE, KEYTAR_ACCOUNT, ALGO, KEY_BYTES
1633
1633
  var init_crypto_store = __esm({
1634
1634
  "src/crypto-store.ts"() {
1635
1635
  "use strict";
1636
- TERMINALHIRE_DIR3 = join4(homedir3(), ".terminalhire");
1636
+ TERMINALHIRE_DIR3 = process.env.TERMINALHIRE_DIR || join4(homedir3(), ".terminalhire");
1637
1637
  KEY_FILE = join4(TERMINALHIRE_DIR3, "key");
1638
1638
  KEYTAR_SERVICE = "terminalhire";
1639
1639
  KEYTAR_ACCOUNT = "profile-key";
@@ -1783,7 +1783,7 @@ var init_profile = __esm({
1783
1783
  "use strict";
1784
1784
  init_src();
1785
1785
  init_crypto_store();
1786
- TERMINALHIRE_DIR4 = join5(homedir4(), ".terminalhire");
1786
+ TERMINALHIRE_DIR4 = process.env.TERMINALHIRE_DIR || join5(homedir4(), ".terminalhire");
1787
1787
  PROFILE_FILE = join5(TERMINALHIRE_DIR4, "profile.enc");
1788
1788
  profileStore = createEncryptedStore(PROFILE_FILE, {
1789
1789
  blank: blankProfile,
@@ -2239,7 +2239,6 @@ init_src();
2239
2239
  import { readFileSync as readFileSync6 } from "fs";
2240
2240
  import { join as join8 } from "path";
2241
2241
  import { homedir as homedir7 } from "os";
2242
- import { createInterface } from "readline";
2243
2242
 
2244
2243
  // bin/cache-store.js
2245
2244
  import { readFileSync as readFileSync2, writeFileSync, mkdirSync, renameSync } from "fs";
@@ -2275,7 +2274,7 @@ function updateIndexCache(patch) {
2275
2274
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync } from "fs";
2276
2275
  import { join as join3 } from "path";
2277
2276
  import { homedir as homedir2 } from "os";
2278
- var TERMINALHIRE_DIR2 = join3(homedir2(), ".terminalhire");
2277
+ var TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join3(homedir2(), ".terminalhire");
2279
2278
  var CONFIG_FILE = join3(TERMINALHIRE_DIR2, "config.json");
2280
2279
  var DEFAULT_CONFIG = {
2281
2280
  nudge: "session",
@@ -2286,8 +2285,7 @@ var DEFAULT_CONFIG = {
2286
2285
  chatShareActivity: false,
2287
2286
  inboundNudgeMuted: false,
2288
2287
  inboundNudgeDisclosed: false,
2289
- contributeEnabled: false,
2290
- contributePrompted: false,
2288
+ contributeEnabled: true,
2291
2289
  betaOptIn: false,
2292
2290
  lastFullFeedbackAt: null,
2293
2291
  lastPulseAskAt: null,
@@ -2304,14 +2302,9 @@ function readConfig() {
2304
2302
  return { ...DEFAULT_CONFIG };
2305
2303
  }
2306
2304
  }
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
2305
  function isContributeEnabled() {
2314
- return readConfig().contributeEnabled === true;
2306
+ const cfg = readConfig();
2307
+ return !(cfg.contributeEnabled === false && !("contributePrompted" in cfg));
2315
2308
  }
2316
2309
 
2317
2310
  // bin/sanitize.js
@@ -2352,7 +2345,7 @@ var INDEX_TTL_MS = 15 * 60 * 1e3;
2352
2345
  var API_URL = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
2353
2346
  var CONTINUITY_RANK_DISABLED = process.env["TERMINALHIRE_NO_CONTINUITY_RANK"] === "1";
2354
2347
  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]";
2348
+ 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
2349
  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
2350
  function readIndexCache() {
2358
2351
  try {
@@ -2377,15 +2370,6 @@ async function fetchIndex(fetchImpl, useCache = true) {
2377
2370
  if (useCache) writeIndexCache(index);
2378
2371
  return index;
2379
2372
  }
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
2373
  var STRONG_THRESHOLD = 0.5;
2390
2374
  if (STRONG_THRESHOLD <= 0) {
2391
2375
  throw new Error("STRONG_THRESHOLD must be > 0 or the contested-issue fail-safe breaks");
@@ -2496,22 +2480,12 @@ ${line3}`;
2496
2480
  }
2497
2481
  async function run(opts = {}) {
2498
2482
  const log = opts.log ?? console.log;
2499
- const isTTY = opts.isTTY ?? Boolean(process.stdin.isTTY);
2500
- const promptImpl = opts.prompt ?? defaultPrompt;
2501
2483
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
2502
2484
  const useCache = opts.fetchImpl == null;
2503
2485
  try {
2504
2486
  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
- }
2487
+ log(CONTRIBUTE_OFF);
2488
+ return;
2515
2489
  }
2516
2490
  const index = await fetchIndex(fetchImpl, useCache);
2517
2491
  const items = Array.isArray(index?.contribute) ? index.contribute : [];
@@ -4054,7 +4054,12 @@ function buildContributionJob(a) {
4054
4054
  // TERM-27: persist the repo's primary language so project curation can
4055
4055
  // exclude the repo's OWN language id (folded into every issue's tags) from
4056
4056
  // the distinct-skill signal. Same `repo` used by the tokenize() tag build.
4057
- language: a.repo.language ?? null
4057
+ language: a.repo.language ?? null,
4058
+ // TERM-35: stamp the search item's comment count (already in the response —
4059
+ // zero extra egress). A NEUTRAL volume signal only: set solely when the
4060
+ // search item carries a finite count >= 0; a failed/absent value leaves it
4061
+ // undefined (never a fabricated 0), so a render's chip falls through cleanly.
4062
+ commentsAtDiscovery: typeof a.issue.comments === "number" && Number.isFinite(a.issue.comments) && a.issue.comments >= 0 ? a.issue.comments : void 0
4058
4063
  },
4059
4064
  // Provenance: repo-first discovered items only (label-first omits the field).
4060
4065
  ...a.discovered ? { discovered: true } : {},
@@ -4570,6 +4575,15 @@ async function enrichWinnability(jobs, contribute, w) {
4570
4575
  );
4571
4576
  }
4572
4577
  }
4578
+ function hasClickableUrl(url) {
4579
+ if (!url) return false;
4580
+ try {
4581
+ const parsed = new URL(url);
4582
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
4583
+ } catch {
4584
+ return false;
4585
+ }
4586
+ }
4573
4587
  async function buildIndex(opts) {
4574
4588
  const includePartners = opts?.includePartners ?? true;
4575
4589
  const publicJobs = await aggregate(opts);
@@ -4580,6 +4594,7 @@ async function buildIndex(opts) {
4580
4594
  ...opts?.partnerRoles ?? []
4581
4595
  ];
4582
4596
  for (const job of partnerJobs) {
4597
+ if (!hasClickableUrl(job.url)) continue;
4583
4598
  if (!seen.has(job.id)) {
4584
4599
  seen.add(job.id);
4585
4600
  allJobs.push(job);
@@ -4661,7 +4676,8 @@ async function fetchIssueStatus(fullName, issueNumber, opts = {}) {
4661
4676
  for (const a of body.assignees ?? []) {
4662
4677
  if (a && typeof a.login === "string") assignees.add(a.login);
4663
4678
  }
4664
- return { state, assignees: [...assignees] };
4679
+ const comments = typeof body.comments === "number" && Number.isFinite(body.comments) && body.comments >= 0 ? body.comments : null;
4680
+ return { state, assignees: [...assignees], comments };
4665
4681
  }
4666
4682
  var GITHUB_API3, DEFAULT_ISSUE_STATUS_TIMEOUT_MS;
4667
4683
  var init_github_issue_status = __esm({
@@ -8598,7 +8614,7 @@ var TERMINALHIRE_DIR2, KEY_FILE, KEYTAR_SERVICE, KEYTAR_ACCOUNT, ALGO, KEY_BYTES
8598
8614
  var init_crypto_store = __esm({
8599
8615
  "src/crypto-store.ts"() {
8600
8616
  "use strict";
8601
- TERMINALHIRE_DIR2 = join3(homedir2(), ".terminalhire");
8617
+ TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join3(homedir2(), ".terminalhire");
8602
8618
  KEY_FILE = join3(TERMINALHIRE_DIR2, "key");
8603
8619
  KEYTAR_SERVICE = "terminalhire";
8604
8620
  KEYTAR_ACCOUNT = "profile-key";
@@ -8748,7 +8764,7 @@ var init_profile = __esm({
8748
8764
  "use strict";
8749
8765
  init_src();
8750
8766
  init_crypto_store();
8751
- TERMINALHIRE_DIR3 = join4(homedir3(), ".terminalhire");
8767
+ TERMINALHIRE_DIR3 = process.env.TERMINALHIRE_DIR || join4(homedir3(), ".terminalhire");
8752
8768
  PROFILE_FILE = join4(TERMINALHIRE_DIR3, "profile.enc");
8753
8769
  profileStore = createEncryptedStore(PROFILE_FILE, {
8754
8770
  blank: blankProfile,
@@ -8894,6 +8910,7 @@ function linkTitle(title, url) {
8894
8910
  // bin/match-slots.js
8895
8911
  var ROTATE_WINDOW_MS = 5 * 60 * 1e3;
8896
8912
  var ROLE_HEAD_ROTATE_MS = 60 * 60 * 1e3;
8913
+ var LEAD_ROTATE_MS = 25 * 60 * 1e3;
8897
8914
  function rotate(list, now, windowMs = ROTATE_WINDOW_MS) {
8898
8915
  if (!Array.isArray(list) || list.length === 0) return [];
8899
8916
  const idx = Math.floor(now / windowMs) % list.length;