terminalhire 0.16.0 → 0.16.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.
@@ -5816,7 +5816,7 @@ async function runInboxTui(deps = {}) {
5816
5816
  const sel = await runInboxPane({ input, output, ...paneDeps });
5817
5817
  if (!sel || sel.action === "quit") break;
5818
5818
  if (sel.action === "open" && sel.login) {
5819
- await openChatPane({ login: sel.login, input, output });
5819
+ await openChatPane({ login: sel.login, input, output, backHint: "inbox" });
5820
5820
  }
5821
5821
  }
5822
5822
  return { ok: true };
@@ -5994,7 +5994,7 @@ function formatPresence(presence, now = /* @__PURE__ */ new Date()) {
5994
5994
  return "\u25D0 reachable";
5995
5995
  }
5996
5996
  function formatThread(state) {
5997
- const { peerLogin, presence, self, messages, inputBuffer, banner } = state;
5997
+ const { peerLogin, presence, self, messages, inputBuffer, banner, backHint } = state;
5998
5998
  const safePeer = sanitizeLine(peerLogin);
5999
5999
  const status = formatPresence(presence);
6000
6000
  const lines = [];
@@ -6022,7 +6022,9 @@ function formatThread(state) {
6022
6022
  }
6023
6023
  lines.push(" " + "\u2500".repeat(56));
6024
6024
  lines.push(` > ${inputBuffer}`);
6025
- lines.push(" Enter send \xB7 Ctrl-S safety number \xB7 /safety \xB7 /block \xB7 q quit");
6025
+ lines.push(
6026
+ backHint ? ` Esc back to ${sanitizeLine(backHint)} \xB7 Enter send \xB7 Ctrl-S safety number \xB7 /safety \xB7 /block \xB7 q quit` : " Enter send \xB7 Ctrl-S safety number \xB7 /safety \xB7 /block \xB7 q quit"
6027
+ );
6026
6028
  return CLEAR2 + lines.join("\n") + "\n";
6027
6029
  }
6028
6030
  function mergeMessages(existing, incoming) {
@@ -6065,7 +6067,10 @@ async function runChatPane(opts = {}) {
6065
6067
  pollIntervalMs = 1500,
6066
6068
  setTimer = (fn, ms) => setInterval(fn, ms),
6067
6069
  clearTimer = (t) => clearInterval(t),
6068
- markRead = defaultMarkThreadRead
6070
+ markRead = defaultMarkThreadRead,
6071
+ // Where Esc returns to, when opened from a parent surface (the inbox TUI
6072
+ // passes 'inbox'). Copy-only — the caller's loop does the actual returning.
6073
+ backHint = null
6069
6074
  } = opts;
6070
6075
  const target = String(login ?? "").replace(/^@/, "").trim();
6071
6076
  if (!target) {
@@ -6195,7 +6200,8 @@ async function runChatPane(opts = {}) {
6195
6200
  self: { login: selfLogin, expired: selfExpired, shareActivity: selfShareActivity },
6196
6201
  messages,
6197
6202
  inputBuffer,
6198
- banner
6203
+ banner,
6204
+ backHint
6199
6205
  })
6200
6206
  );
6201
6207
  }
@@ -6388,6 +6394,11 @@ async function runChatPane(opts = {}) {
6388
6394
  function onData(chunk) {
6389
6395
  if (cleaned) return;
6390
6396
  const s = chunk.toString("utf8");
6397
+ if (s === KEY_ESC2) {
6398
+ finish("back");
6399
+ return;
6400
+ }
6401
+ if (s.charCodeAt(0) === 27) return;
6391
6402
  for (const ch of s) {
6392
6403
  if (ch === KEY_CTRL_C2) {
6393
6404
  finish("sigint");
@@ -6629,7 +6640,7 @@ async function run2() {
6629
6640
  process.exit(1);
6630
6641
  }
6631
6642
  }
6632
- var CHAT_BASE3, GH_SESSION_COOKIE4, HIDE_CURSOR2, SHOW_CURSOR2, ENTER_ALT2, EXIT_ALT2, CLEAR2, KEY_CTRL_C2, KEY_CTRL_S, KEY_ENTER_A2, KEY_ENTER_B2, KEY_BACKSPACE_A2, KEY_BACKSPACE_B2, MAX_INPUT_LEN, ANSI_CSI, ANSI_OSC, ANSI_OTHER, C0_C1_DEL, CHAT_DISCLOSURE, CHAT_AT_REST, CHAT_CODE_OF_CONDUCT, CHAT_MIN_AGE, DEPOSIT_CTA, ACTIVE_WINDOW_MS;
6643
+ var CHAT_BASE3, GH_SESSION_COOKIE4, HIDE_CURSOR2, SHOW_CURSOR2, ENTER_ALT2, EXIT_ALT2, CLEAR2, KEY_CTRL_C2, KEY_ESC2, KEY_CTRL_S, KEY_ENTER_A2, KEY_ENTER_B2, KEY_BACKSPACE_A2, KEY_BACKSPACE_B2, MAX_INPUT_LEN, ANSI_CSI, ANSI_OSC, ANSI_OTHER, C0_C1_DEL, CHAT_DISCLOSURE, CHAT_AT_REST, CHAT_CODE_OF_CONDUCT, CHAT_MIN_AGE, DEPOSIT_CTA, ACTIVE_WINDOW_MS;
6633
6644
  var init_jpi_chat = __esm({
6634
6645
  "bin/jpi-chat.js"() {
6635
6646
  init_chat_client();
@@ -6643,6 +6654,7 @@ var init_jpi_chat = __esm({
6643
6654
  EXIT_ALT2 = "\x1B[?1049l";
6644
6655
  CLEAR2 = "\x1B[2J\x1B[H";
6645
6656
  KEY_CTRL_C2 = "";
6657
+ KEY_ESC2 = "\x1B";
6646
6658
  KEY_CTRL_S = "";
6647
6659
  KEY_ENTER_A2 = "\r";
6648
6660
  KEY_ENTER_B2 = "\n";
@@ -76,6 +76,7 @@ var config_exports = {};
76
76
  __export(config_exports, {
77
77
  getNudgeMode: () => getNudgeMode,
78
78
  isContributeEnabled: () => isContributeEnabled,
79
+ isContributePrompted: () => isContributePrompted,
79
80
  isInboundNudgeMuted: () => isInboundNudgeMuted,
80
81
  isPeerConnectEnabled: () => isPeerConnectEnabled,
81
82
  parseNudgeMode: () => parseNudgeMode,
@@ -128,6 +129,9 @@ function isInboundNudgeMuted() {
128
129
  function isContributeEnabled() {
129
130
  return readConfig().contributeEnabled === true;
130
131
  }
132
+ function isContributePrompted() {
133
+ return readConfig().contributePrompted === true;
134
+ }
131
135
  var TERMINALHIRE_DIR, CONFIG_FILE, DEFAULT_CONFIG;
132
136
  var init_config = __esm({
133
137
  "src/config.ts"() {
@@ -12026,7 +12030,7 @@ async function runInboxTui(deps = {}) {
12026
12030
  const sel = await runInboxPane({ input, output, ...paneDeps });
12027
12031
  if (!sel || sel.action === "quit") break;
12028
12032
  if (sel.action === "open" && sel.login) {
12029
- await openChatPane({ login: sel.login, input, output });
12033
+ await openChatPane({ login: sel.login, input, output, backHint: "inbox" });
12030
12034
  }
12031
12035
  }
12032
12036
  return { ok: true };
@@ -12224,7 +12228,7 @@ function formatPresence(presence, now = /* @__PURE__ */ new Date()) {
12224
12228
  return "\u25D0 reachable";
12225
12229
  }
12226
12230
  function formatThread(state) {
12227
- const { peerLogin, presence, self, messages, inputBuffer, banner } = state;
12231
+ const { peerLogin, presence, self, messages, inputBuffer, banner, backHint } = state;
12228
12232
  const safePeer = sanitizeLine(peerLogin);
12229
12233
  const status = formatPresence(presence);
12230
12234
  const lines = [];
@@ -12252,7 +12256,9 @@ function formatThread(state) {
12252
12256
  }
12253
12257
  lines.push(" " + "\u2500".repeat(56));
12254
12258
  lines.push(` > ${inputBuffer}`);
12255
- lines.push(" Enter send \xB7 Ctrl-S safety number \xB7 /safety \xB7 /block \xB7 q quit");
12259
+ lines.push(
12260
+ backHint ? ` Esc back to ${sanitizeLine(backHint)} \xB7 Enter send \xB7 Ctrl-S safety number \xB7 /safety \xB7 /block \xB7 q quit` : " Enter send \xB7 Ctrl-S safety number \xB7 /safety \xB7 /block \xB7 q quit"
12261
+ );
12256
12262
  return CLEAR2 + lines.join("\n") + "\n";
12257
12263
  }
12258
12264
  function mergeMessages(existing, incoming) {
@@ -12295,7 +12301,10 @@ async function runChatPane(opts = {}) {
12295
12301
  pollIntervalMs = 1500,
12296
12302
  setTimer = (fn, ms) => setInterval(fn, ms),
12297
12303
  clearTimer = (t) => clearInterval(t),
12298
- markRead = defaultMarkThreadRead
12304
+ markRead = defaultMarkThreadRead,
12305
+ // Where Esc returns to, when opened from a parent surface (the inbox TUI
12306
+ // passes 'inbox'). Copy-only — the caller's loop does the actual returning.
12307
+ backHint = null
12299
12308
  } = opts;
12300
12309
  const target = String(login ?? "").replace(/^@/, "").trim();
12301
12310
  if (!target) {
@@ -12425,7 +12434,8 @@ async function runChatPane(opts = {}) {
12425
12434
  self: { login: selfLogin, expired: selfExpired, shareActivity: selfShareActivity },
12426
12435
  messages,
12427
12436
  inputBuffer,
12428
- banner
12437
+ banner,
12438
+ backHint
12429
12439
  })
12430
12440
  );
12431
12441
  }
@@ -12618,6 +12628,11 @@ async function runChatPane(opts = {}) {
12618
12628
  function onData(chunk) {
12619
12629
  if (cleaned) return;
12620
12630
  const s = chunk.toString("utf8");
12631
+ if (s === KEY_ESC2) {
12632
+ finish("back");
12633
+ return;
12634
+ }
12635
+ if (s.charCodeAt(0) === 27) return;
12621
12636
  for (const ch of s) {
12622
12637
  if (ch === KEY_CTRL_C2) {
12623
12638
  finish("sigint");
@@ -12859,7 +12874,7 @@ async function run11() {
12859
12874
  process.exit(1);
12860
12875
  }
12861
12876
  }
12862
- var CHAT_BASE3, GH_SESSION_COOKIE5, HIDE_CURSOR2, SHOW_CURSOR2, ENTER_ALT2, EXIT_ALT2, CLEAR2, KEY_CTRL_C2, KEY_CTRL_S, KEY_ENTER_A2, KEY_ENTER_B2, KEY_BACKSPACE_A2, KEY_BACKSPACE_B2, MAX_INPUT_LEN, ANSI_CSI, ANSI_OSC, ANSI_OTHER, C0_C1_DEL, CHAT_DISCLOSURE, CHAT_AT_REST, CHAT_CODE_OF_CONDUCT, CHAT_MIN_AGE, DEPOSIT_CTA, ACTIVE_WINDOW_MS;
12877
+ var CHAT_BASE3, GH_SESSION_COOKIE5, HIDE_CURSOR2, SHOW_CURSOR2, ENTER_ALT2, EXIT_ALT2, CLEAR2, KEY_CTRL_C2, KEY_ESC2, KEY_CTRL_S, KEY_ENTER_A2, KEY_ENTER_B2, KEY_BACKSPACE_A2, KEY_BACKSPACE_B2, MAX_INPUT_LEN, ANSI_CSI, ANSI_OSC, ANSI_OTHER, C0_C1_DEL, CHAT_DISCLOSURE, CHAT_AT_REST, CHAT_CODE_OF_CONDUCT, CHAT_MIN_AGE, DEPOSIT_CTA, ACTIVE_WINDOW_MS;
12863
12878
  var init_jpi_chat = __esm({
12864
12879
  "bin/jpi-chat.js"() {
12865
12880
  "use strict";
@@ -12874,6 +12889,7 @@ var init_jpi_chat = __esm({
12874
12889
  EXIT_ALT2 = "\x1B[?1049l";
12875
12890
  CLEAR2 = "\x1B[2J\x1B[H";
12876
12891
  KEY_CTRL_C2 = "";
12892
+ KEY_ESC2 = "\x1B";
12877
12893
  KEY_CTRL_S = "";
12878
12894
  KEY_ENTER_A2 = "\r";
12879
12895
  KEY_ENTER_B2 = "\n";
@@ -13994,17 +14010,32 @@ function buildIncomingIntroLine(incomingPending) {
13994
14010
  if (n < 1) return null;
13995
14011
  return n === 1 ? `\u2198 someone wants to connect \xB7 terminalhire intro --list` : `\u2198 ${n} people want to connect \xB7 terminalhire intro --list`;
13996
14012
  }
14013
+ function buildContributeNudgeLine(contributeNudge) {
14014
+ const n = contributeNudge && typeof contributeNudge.count === "number" ? contributeNudge.count : 0;
14015
+ if (n < 1) return null;
14016
+ return n === 1 ? "\u2726 an open-source issue that counts toward your r\xE9sum\xE9 \xB7 terminalhire contribute" : `\u2726 ${n} open-source issues that count toward your r\xE9sum\xE9 \xB7 terminalhire contribute`;
14017
+ }
13997
14018
  function buildSessionStaleLine(sessionStale) {
13998
14019
  return sessionStale === true ? "\u26A0 terminalhire: linked session expired \u2014 run: terminalhire login" : null;
13999
14020
  }
14000
14021
  function buildSpinnerPool(topMatches, _max = 6, opts = {}) {
14001
- const { sessionTags, frequency = "always", topPeers, incomingPending, sessionStale, seenHistory } = opts;
14022
+ const {
14023
+ sessionTags,
14024
+ frequency = "always",
14025
+ topPeers,
14026
+ incomingPending,
14027
+ sessionStale,
14028
+ contributeNudge,
14029
+ seenHistory
14030
+ } = opts;
14002
14031
  const staleLine = buildSessionStaleLine(sessionStale);
14003
14032
  const withStale = (pool2) => staleLine ? [staleLine, ...pool2] : pool2;
14004
14033
  const introLine = buildIncomingIntroLine(incomingPending);
14034
+ const contributeLine = buildContributeNudgeLine(contributeNudge);
14005
14035
  const ranked = filterFreshMatches(rankBySessionTags(topMatches, sessionTags), seenHistory);
14006
14036
  if (!Array.isArray(ranked) || ranked.length === 0) {
14007
14037
  if (introLine) return withStale([introLine]);
14038
+ if (contributeLine) return withStale([contributeLine]);
14008
14039
  const peerLine = buildPeerLine(topPeers);
14009
14040
  return withStale(peerLine ? [peerLine] : []);
14010
14041
  }
@@ -14012,6 +14043,7 @@ function buildSpinnerPool(topMatches, _max = 6, opts = {}) {
14012
14043
  const cap = Math.max(1, verbCountForFrequency(frequency, headers.length));
14013
14044
  const pool = [...headers.slice(0, cap), ctaVerb()];
14014
14045
  if (introLine) pool.push(introLine);
14046
+ if (contributeLine) pool.push(contributeLine);
14015
14047
  return withStale(pool);
14016
14048
  }
14017
14049
  var VERB_INTROS;
@@ -14146,6 +14178,7 @@ function renderRefreshSurface(topMatches, sc, opts = {}) {
14146
14178
  topPeers: opts.topPeers,
14147
14179
  incomingPending: opts.incomingPending,
14148
14180
  sessionStale: opts.sessionStale,
14181
+ contributeNudge: opts.contributeNudge,
14149
14182
  seenHistory
14150
14183
  });
14151
14184
  if (verbs.length > 0) applySpinnerVerbs(verbs, sc.mode);
@@ -14176,6 +14209,7 @@ __export(spinner_exports, {
14176
14209
  applySpinnerTips: () => applySpinnerTips,
14177
14210
  applySpinnerVerbs: () => applySpinnerVerbs,
14178
14211
  buildContextVerbs: () => buildContextVerbs,
14212
+ buildContributeNudgeLine: () => buildContributeNudgeLine,
14179
14213
  buildIncomingIntroLine: () => buildIncomingIntroLine,
14180
14214
  buildPeerLine: () => buildPeerLine,
14181
14215
  buildSessionStaleLine: () => buildSessionStaleLine,
@@ -14207,6 +14241,7 @@ var init_spinner = __esm({
14207
14241
  init_spinner_verbs();
14208
14242
  init_spinner_verbs();
14209
14243
  init_spinner_verbs();
14244
+ init_spinner_verbs();
14210
14245
  init_spinner_select();
14211
14246
  init_spinner_select();
14212
14247
  init_spinner_select();
@@ -15002,6 +15037,7 @@ async function run20() {
15002
15037
  }
15003
15038
  const jobs = index?.jobs ?? [];
15004
15039
  const contribute = isContributeEnabled() && Array.isArray(index?.contribute) ? index.contribute : [];
15040
+ const contributeNudge = !isContributeEnabled() && !isContributePrompted() && Array.isArray(index?.contribute) && index.contribute.length > 0 ? { count: index.contribute.length } : null;
15005
15041
  let matchCount = 0;
15006
15042
  let topMatches = [];
15007
15043
  let widenReserve = [];
@@ -15152,7 +15188,12 @@ async function run20() {
15152
15188
  topPeers,
15153
15189
  incomingPending,
15154
15190
  unreadChat,
15155
- sessionStale
15191
+ sessionStale,
15192
+ // In-process-only despite being persisted: the render pass receives it as
15193
+ // a function argument below. A future cache READER must not gate on this
15194
+ // field without re-checking isContributeEnabled/isContributePrompted at
15195
+ // read time — the persisted copy can be stale relative to the answer.
15196
+ contributeNudge
15156
15197
  };
15157
15198
  writeFileSync20(INDEX_CACHE_FILE7, JSON.stringify(cacheEntry), "utf8");
15158
15199
  try {
@@ -15182,6 +15223,7 @@ async function run20() {
15182
15223
  topPeers,
15183
15224
  incomingPending,
15184
15225
  sessionStale,
15226
+ contributeNudge,
15185
15227
  baseUrl: API_URL7,
15186
15228
  seenHistory,
15187
15229
  widen
@@ -5058,7 +5058,7 @@ function formatPresence(presence, now = /* @__PURE__ */ new Date()) {
5058
5058
  return "\u25D0 reachable";
5059
5059
  }
5060
5060
  function formatThread(state) {
5061
- const { peerLogin, presence, self, messages, inputBuffer, banner } = state;
5061
+ const { peerLogin, presence, self, messages, inputBuffer, banner, backHint } = state;
5062
5062
  const safePeer = sanitizeLine(peerLogin);
5063
5063
  const status = formatPresence(presence);
5064
5064
  const lines = [];
@@ -5086,7 +5086,9 @@ function formatThread(state) {
5086
5086
  }
5087
5087
  lines.push(" " + "\u2500".repeat(56));
5088
5088
  lines.push(` > ${inputBuffer}`);
5089
- lines.push(" Enter send \xB7 Ctrl-S safety number \xB7 /safety \xB7 /block \xB7 q quit");
5089
+ lines.push(
5090
+ backHint ? ` Esc back to ${sanitizeLine(backHint)} \xB7 Enter send \xB7 Ctrl-S safety number \xB7 /safety \xB7 /block \xB7 q quit` : " Enter send \xB7 Ctrl-S safety number \xB7 /safety \xB7 /block \xB7 q quit"
5091
+ );
5090
5092
  return CLEAR + lines.join("\n") + "\n";
5091
5093
  }
5092
5094
  function mergeMessages(existing, incoming) {
@@ -5129,7 +5131,10 @@ async function runChatPane(opts = {}) {
5129
5131
  pollIntervalMs = 1500,
5130
5132
  setTimer = (fn, ms) => setInterval(fn, ms),
5131
5133
  clearTimer = (t) => clearInterval(t),
5132
- markRead = defaultMarkThreadRead
5134
+ markRead = defaultMarkThreadRead,
5135
+ // Where Esc returns to, when opened from a parent surface (the inbox TUI
5136
+ // passes 'inbox'). Copy-only — the caller's loop does the actual returning.
5137
+ backHint = null
5133
5138
  } = opts;
5134
5139
  const target = String(login ?? "").replace(/^@/, "").trim();
5135
5140
  if (!target) {
@@ -5259,7 +5264,8 @@ async function runChatPane(opts = {}) {
5259
5264
  self: { login: selfLogin, expired: selfExpired, shareActivity: selfShareActivity },
5260
5265
  messages,
5261
5266
  inputBuffer,
5262
- banner
5267
+ banner,
5268
+ backHint
5263
5269
  })
5264
5270
  );
5265
5271
  }
@@ -5452,6 +5458,11 @@ async function runChatPane(opts = {}) {
5452
5458
  function onData(chunk) {
5453
5459
  if (cleaned) return;
5454
5460
  const s = chunk.toString("utf8");
5461
+ if (s === KEY_ESC) {
5462
+ finish("back");
5463
+ return;
5464
+ }
5465
+ if (s.charCodeAt(0) === 27) return;
5455
5466
  for (const ch of s) {
5456
5467
  if (ch === KEY_CTRL_C) {
5457
5468
  finish("sigint");
@@ -5523,7 +5534,7 @@ async function runChatPane(opts = {}) {
5523
5534
  }
5524
5535
  });
5525
5536
  }
5526
- var CHAT_BASE3, GH_SESSION_COOKIE3, HIDE_CURSOR, SHOW_CURSOR, ENTER_ALT, EXIT_ALT, CLEAR, KEY_CTRL_C, KEY_CTRL_S, KEY_ENTER_A, KEY_ENTER_B, KEY_BACKSPACE_A, KEY_BACKSPACE_B, MAX_INPUT_LEN, ANSI_CSI, ANSI_OSC, ANSI_OTHER, C0_C1_DEL, CHAT_DISCLOSURE, CHAT_AT_REST, CHAT_CODE_OF_CONDUCT, CHAT_MIN_AGE, DEPOSIT_CTA, ACTIVE_WINDOW_MS;
5537
+ var CHAT_BASE3, GH_SESSION_COOKIE3, HIDE_CURSOR, SHOW_CURSOR, ENTER_ALT, EXIT_ALT, CLEAR, KEY_CTRL_C, KEY_ESC, KEY_CTRL_S, KEY_ENTER_A, KEY_ENTER_B, KEY_BACKSPACE_A, KEY_BACKSPACE_B, MAX_INPUT_LEN, ANSI_CSI, ANSI_OSC, ANSI_OTHER, C0_C1_DEL, CHAT_DISCLOSURE, CHAT_AT_REST, CHAT_CODE_OF_CONDUCT, CHAT_MIN_AGE, DEPOSIT_CTA, ACTIVE_WINDOW_MS;
5527
5538
  var init_jpi_chat = __esm({
5528
5539
  "bin/jpi-chat.js"() {
5529
5540
  "use strict";
@@ -5538,6 +5549,7 @@ var init_jpi_chat = __esm({
5538
5549
  EXIT_ALT = "\x1B[?1049l";
5539
5550
  CLEAR = "\x1B[2J\x1B[H";
5540
5551
  KEY_CTRL_C = "";
5552
+ KEY_ESC = "\x1B";
5541
5553
  KEY_CTRL_S = "";
5542
5554
  KEY_ENTER_A = "\r";
5543
5555
  KEY_ENTER_B = "\n";
@@ -6308,7 +6320,7 @@ async function runInboxPane(opts = {}) {
6308
6320
  finish({ action: "quit" });
6309
6321
  return;
6310
6322
  }
6311
- if (s === KEY_ESC) {
6323
+ if (s === KEY_ESC2) {
6312
6324
  if (mode === "input" || mode === "confirm") cancelMode();
6313
6325
  else finish({ action: "quit" });
6314
6326
  return;
@@ -6433,7 +6445,7 @@ async function runInboxTui(deps = {}) {
6433
6445
  const sel = await runInboxPane({ input, output, ...paneDeps });
6434
6446
  if (!sel || sel.action === "quit") break;
6435
6447
  if (sel.action === "open" && sel.login) {
6436
- await openChatPane({ login: sel.login, input, output });
6448
+ await openChatPane({ login: sel.login, input, output, backHint: "inbox" });
6437
6449
  }
6438
6450
  }
6439
6451
  return { ok: true };
@@ -6455,7 +6467,7 @@ async function run(opts = {}) {
6455
6467
  await mod2.runInbox({});
6456
6468
  }
6457
6469
  }
6458
- var HIDE_CURSOR2, SHOW_CURSOR2, ENTER_ALT2, EXIT_ALT2, CLEAR2, INVERSE, RESET, BOLD, BOLD_OFF, KEY_CTRL_C2, KEY_ESC, KEY_UP, KEY_DOWN, KEY_ENTER_A2, KEY_ENTER_B2, KEY_BACKSPACE_A2, KEY_BACKSPACE_B2, MAX_CONTACT_LEN, DEFAULT_REFRESH_MS;
6470
+ var HIDE_CURSOR2, SHOW_CURSOR2, ENTER_ALT2, EXIT_ALT2, CLEAR2, INVERSE, RESET, BOLD, BOLD_OFF, KEY_CTRL_C2, KEY_ESC2, KEY_UP, KEY_DOWN, KEY_ENTER_A2, KEY_ENTER_B2, KEY_BACKSPACE_A2, KEY_BACKSPACE_B2, MAX_CONTACT_LEN, DEFAULT_REFRESH_MS;
6459
6471
  var init_jpi_inbox = __esm({
6460
6472
  "bin/jpi-inbox.js"() {
6461
6473
  init_chat_client();
@@ -6471,7 +6483,7 @@ var init_jpi_inbox = __esm({
6471
6483
  BOLD = "\x1B[1m";
6472
6484
  BOLD_OFF = "\x1B[22m";
6473
6485
  KEY_CTRL_C2 = "";
6474
- KEY_ESC = "\x1B";
6486
+ KEY_ESC2 = "\x1B";
6475
6487
  KEY_UP = "\x1B[A";
6476
6488
  KEY_DOWN = "\x1B[B";
6477
6489
  KEY_ENTER_A2 = "\r";
@@ -7319,17 +7319,32 @@ function buildIncomingIntroLine(incomingPending) {
7319
7319
  if (n < 1) return null;
7320
7320
  return n === 1 ? `\u2198 someone wants to connect \xB7 terminalhire intro --list` : `\u2198 ${n} people want to connect \xB7 terminalhire intro --list`;
7321
7321
  }
7322
+ function buildContributeNudgeLine(contributeNudge) {
7323
+ const n = contributeNudge && typeof contributeNudge.count === "number" ? contributeNudge.count : 0;
7324
+ if (n < 1) return null;
7325
+ return n === 1 ? "\u2726 an open-source issue that counts toward your r\xE9sum\xE9 \xB7 terminalhire contribute" : `\u2726 ${n} open-source issues that count toward your r\xE9sum\xE9 \xB7 terminalhire contribute`;
7326
+ }
7322
7327
  function buildSessionStaleLine(sessionStale) {
7323
7328
  return sessionStale === true ? "\u26A0 terminalhire: linked session expired \u2014 run: terminalhire login" : null;
7324
7329
  }
7325
7330
  function buildSpinnerPool(topMatches, _max = 6, opts = {}) {
7326
- const { sessionTags, frequency = "always", topPeers, incomingPending, sessionStale, seenHistory } = opts;
7331
+ const {
7332
+ sessionTags,
7333
+ frequency = "always",
7334
+ topPeers,
7335
+ incomingPending,
7336
+ sessionStale,
7337
+ contributeNudge,
7338
+ seenHistory
7339
+ } = opts;
7327
7340
  const staleLine = buildSessionStaleLine(sessionStale);
7328
7341
  const withStale = (pool2) => staleLine ? [staleLine, ...pool2] : pool2;
7329
7342
  const introLine = buildIncomingIntroLine(incomingPending);
7343
+ const contributeLine = buildContributeNudgeLine(contributeNudge);
7330
7344
  const ranked = filterFreshMatches(rankBySessionTags(topMatches, sessionTags), seenHistory);
7331
7345
  if (!Array.isArray(ranked) || ranked.length === 0) {
7332
7346
  if (introLine) return withStale([introLine]);
7347
+ if (contributeLine) return withStale([contributeLine]);
7333
7348
  const peerLine = buildPeerLine(topPeers);
7334
7349
  return withStale(peerLine ? [peerLine] : []);
7335
7350
  }
@@ -7337,6 +7352,7 @@ function buildSpinnerPool(topMatches, _max = 6, opts = {}) {
7337
7352
  const cap = Math.max(1, verbCountForFrequency(frequency, headers.length));
7338
7353
  const pool = [...headers.slice(0, cap), ctaVerb()];
7339
7354
  if (introLine) pool.push(introLine);
7355
+ if (contributeLine) pool.push(contributeLine);
7340
7356
  return withStale(pool);
7341
7357
  }
7342
7358
  var VERB_INTROS;
@@ -7471,6 +7487,7 @@ function renderRefreshSurface(topMatches, sc, opts = {}) {
7471
7487
  topPeers: opts.topPeers,
7472
7488
  incomingPending: opts.incomingPending,
7473
7489
  sessionStale: opts.sessionStale,
7490
+ contributeNudge: opts.contributeNudge,
7474
7491
  seenHistory
7475
7492
  });
7476
7493
  if (verbs.length > 0) applySpinnerVerbs(verbs, sc.mode);
@@ -7501,6 +7518,7 @@ __export(spinner_exports, {
7501
7518
  applySpinnerTips: () => applySpinnerTips,
7502
7519
  applySpinnerVerbs: () => applySpinnerVerbs,
7503
7520
  buildContextVerbs: () => buildContextVerbs,
7521
+ buildContributeNudgeLine: () => buildContributeNudgeLine,
7504
7522
  buildIncomingIntroLine: () => buildIncomingIntroLine,
7505
7523
  buildPeerLine: () => buildPeerLine,
7506
7524
  buildSessionStaleLine: () => buildSessionStaleLine,
@@ -7532,6 +7550,7 @@ var init_spinner = __esm({
7532
7550
  init_spinner_verbs();
7533
7551
  init_spinner_verbs();
7534
7552
  init_spinner_verbs();
7553
+ init_spinner_verbs();
7535
7554
  init_spinner_select();
7536
7555
  init_spinner_select();
7537
7556
  init_spinner_select();
@@ -7742,6 +7761,9 @@ function isInboundNudgeMuted() {
7742
7761
  function isContributeEnabled() {
7743
7762
  return readConfig().contributeEnabled === true;
7744
7763
  }
7764
+ function isContributePrompted() {
7765
+ return readConfig().contributePrompted === true;
7766
+ }
7745
7767
 
7746
7768
  // src/web-session.ts
7747
7769
  import {
@@ -7806,6 +7828,7 @@ async function run() {
7806
7828
  }
7807
7829
  const jobs = index?.jobs ?? [];
7808
7830
  const contribute = isContributeEnabled() && Array.isArray(index?.contribute) ? index.contribute : [];
7831
+ const contributeNudge = !isContributeEnabled() && !isContributePrompted() && Array.isArray(index?.contribute) && index.contribute.length > 0 ? { count: index.contribute.length } : null;
7809
7832
  let matchCount = 0;
7810
7833
  let topMatches = [];
7811
7834
  let widenReserve = [];
@@ -7956,7 +7979,12 @@ async function run() {
7956
7979
  topPeers,
7957
7980
  incomingPending,
7958
7981
  unreadChat,
7959
- sessionStale
7982
+ sessionStale,
7983
+ // In-process-only despite being persisted: the render pass receives it as
7984
+ // a function argument below. A future cache READER must not gate on this
7985
+ // field without re-checking isContributeEnabled/isContributePrompted at
7986
+ // read time — the persisted copy can be stale relative to the answer.
7987
+ contributeNudge
7960
7988
  };
7961
7989
  writeFileSync7(INDEX_CACHE_FILE2, JSON.stringify(cacheEntry), "utf8");
7962
7990
  try {
@@ -7986,6 +8014,7 @@ async function run() {
7986
8014
  topPeers,
7987
8015
  incomingPending,
7988
8016
  sessionStale,
8017
+ contributeNudge,
7989
8018
  baseUrl: API_URL2,
7990
8019
  seenHistory,
7991
8020
  widen
@@ -241,17 +241,32 @@ function buildIncomingIntroLine(incomingPending) {
241
241
  if (n < 1) return null;
242
242
  return n === 1 ? `\u2198 someone wants to connect \xB7 terminalhire intro --list` : `\u2198 ${n} people want to connect \xB7 terminalhire intro --list`;
243
243
  }
244
+ function buildContributeNudgeLine(contributeNudge) {
245
+ const n = contributeNudge && typeof contributeNudge.count === "number" ? contributeNudge.count : 0;
246
+ if (n < 1) return null;
247
+ return n === 1 ? "\u2726 an open-source issue that counts toward your r\xE9sum\xE9 \xB7 terminalhire contribute" : `\u2726 ${n} open-source issues that count toward your r\xE9sum\xE9 \xB7 terminalhire contribute`;
248
+ }
244
249
  function buildSessionStaleLine(sessionStale) {
245
250
  return sessionStale === true ? "\u26A0 terminalhire: linked session expired \u2014 run: terminalhire login" : null;
246
251
  }
247
252
  function buildSpinnerPool(topMatches, _max = 6, opts = {}) {
248
- const { sessionTags, frequency = "always", topPeers, incomingPending, sessionStale, seenHistory } = opts;
253
+ const {
254
+ sessionTags,
255
+ frequency = "always",
256
+ topPeers,
257
+ incomingPending,
258
+ sessionStale,
259
+ contributeNudge,
260
+ seenHistory
261
+ } = opts;
249
262
  const staleLine = buildSessionStaleLine(sessionStale);
250
263
  const withStale = (pool2) => staleLine ? [staleLine, ...pool2] : pool2;
251
264
  const introLine = buildIncomingIntroLine(incomingPending);
265
+ const contributeLine = buildContributeNudgeLine(contributeNudge);
252
266
  const ranked = filterFreshMatches(rankBySessionTags(topMatches, sessionTags), seenHistory);
253
267
  if (!Array.isArray(ranked) || ranked.length === 0) {
254
268
  if (introLine) return withStale([introLine]);
269
+ if (contributeLine) return withStale([contributeLine]);
255
270
  const peerLine = buildPeerLine(topPeers);
256
271
  return withStale(peerLine ? [peerLine] : []);
257
272
  }
@@ -259,6 +274,7 @@ function buildSpinnerPool(topMatches, _max = 6, opts = {}) {
259
274
  const cap = Math.max(1, verbCountForFrequency(frequency, headers.length));
260
275
  const pool = [...headers.slice(0, cap), ctaVerb()];
261
276
  if (introLine) pool.push(introLine);
277
+ if (contributeLine) pool.push(contributeLine);
262
278
  return withStale(pool);
263
279
  }
264
280
 
@@ -39,11 +39,11 @@ function render() {
39
39
  if (incoming > 0) parts.push(`\u2709 ${incoming} intro request${incoming === 1 ? "" : "s"}`);
40
40
  if (parts.length > 0) {
41
41
  let line = parts.join(" \xB7 ");
42
- line += unread > 0 ? " \u2014 run: terminalhire chat" : " \u2014 run: terminalhire intro --list";
42
+ line += " \u2014 run: th inbox";
43
43
  return line;
44
44
  }
45
45
  if (stale) {
46
- return "\u26A0 terminalhire session expired \u2014 run: terminalhire link";
46
+ return "\u26A0 terminalhire session expired \u2014 run: th link";
47
47
  }
48
48
  return "";
49
49
  } catch {
package/dist/bin/jpi.js CHANGED
@@ -264,17 +264,17 @@ try {
264
264
  let line;
265
265
  if (haveRoles) {
266
266
  const plural = matchCount === 1 ? "role" : "roles";
267
- line = `\u2726 ${matchCount} ${plural} match your current work \u2014 run: terminalhire jobs`;
267
+ line = `\u2726 ${matchCount} ${plural} match your current work \u2014 run: th jobs`;
268
268
  if (incomingCount > 0) line += ` \xB7 \u2709 ${incomingCount} intro request${incomingCount === 1 ? "" : "s"}`;
269
269
  if (unreadChatCount > 0) line += ` \xB7 \u{1F4AC} ${unreadChatCount} unread`;
270
- if (sessionStale) line += ` \xB7 \u26A0 session expired \u2014 run: terminalhire link`;
270
+ if (sessionStale) line += ` \xB7 \u26A0 session expired \u2014 run: th link`;
271
271
  } else if (incomingCount > 0) {
272
- line = `\u2709 ${incomingCount} intro request${incomingCount === 1 ? "" : "s"} \u2014 run: terminalhire intro --list`;
272
+ line = `\u2709 ${incomingCount} intro request${incomingCount === 1 ? "" : "s"} \u2014 run: th inbox`;
273
273
  if (unreadChatCount > 0) line += ` \xB7 \u{1F4AC} ${unreadChatCount} unread`;
274
274
  } else if (unreadChatCount > 0) {
275
- line = `\u{1F4AC} ${unreadChatCount} unread \u2014 run: terminalhire chat`;
275
+ line = `\u{1F4AC} ${unreadChatCount} unread \u2014 run: th inbox`;
276
276
  } else {
277
- line = `\u26A0 terminalhire session expired \u2014 run: terminalhire link to restore your connection signals`;
277
+ line = `\u26A0 terminalhire session expired \u2014 run: th link to restore your connection signals`;
278
278
  }
279
279
  process.stdout.write(line + "\n");
280
280
  if (haveRoles && nudgeMode === "session") {
@@ -357,17 +357,32 @@ function buildIncomingIntroLine(incomingPending) {
357
357
  if (n < 1) return null;
358
358
  return n === 1 ? `\u2198 someone wants to connect \xB7 terminalhire intro --list` : `\u2198 ${n} people want to connect \xB7 terminalhire intro --list`;
359
359
  }
360
+ function buildContributeNudgeLine(contributeNudge) {
361
+ const n = contributeNudge && typeof contributeNudge.count === "number" ? contributeNudge.count : 0;
362
+ if (n < 1) return null;
363
+ return n === 1 ? "\u2726 an open-source issue that counts toward your r\xE9sum\xE9 \xB7 terminalhire contribute" : `\u2726 ${n} open-source issues that count toward your r\xE9sum\xE9 \xB7 terminalhire contribute`;
364
+ }
360
365
  function buildSessionStaleLine(sessionStale) {
361
366
  return sessionStale === true ? "\u26A0 terminalhire: linked session expired \u2014 run: terminalhire login" : null;
362
367
  }
363
368
  function buildSpinnerPool(topMatches, _max = 6, opts = {}) {
364
- const { sessionTags, frequency = "always", topPeers, incomingPending, sessionStale, seenHistory } = opts;
369
+ const {
370
+ sessionTags,
371
+ frequency = "always",
372
+ topPeers,
373
+ incomingPending,
374
+ sessionStale,
375
+ contributeNudge,
376
+ seenHistory
377
+ } = opts;
365
378
  const staleLine = buildSessionStaleLine(sessionStale);
366
379
  const withStale = (pool2) => staleLine ? [staleLine, ...pool2] : pool2;
367
380
  const introLine = buildIncomingIntroLine(incomingPending);
381
+ const contributeLine = buildContributeNudgeLine(contributeNudge);
368
382
  const ranked = filterFreshMatches(rankBySessionTags(topMatches, sessionTags), seenHistory);
369
383
  if (!Array.isArray(ranked) || ranked.length === 0) {
370
384
  if (introLine) return withStale([introLine]);
385
+ if (contributeLine) return withStale([contributeLine]);
371
386
  const peerLine = buildPeerLine(topPeers);
372
387
  return withStale(peerLine ? [peerLine] : []);
373
388
  }
@@ -375,6 +390,7 @@ function buildSpinnerPool(topMatches, _max = 6, opts = {}) {
375
390
  const cap = Math.max(1, verbCountForFrequency(frequency, headers.length));
376
391
  const pool = [...headers.slice(0, cap), ctaVerb()];
377
392
  if (introLine) pool.push(introLine);
393
+ if (contributeLine) pool.push(contributeLine);
378
394
  return withStale(pool);
379
395
  }
380
396
 
@@ -501,6 +517,7 @@ function renderRefreshSurface(topMatches, sc, opts = {}) {
501
517
  topPeers: opts.topPeers,
502
518
  incomingPending: opts.incomingPending,
503
519
  sessionStale: opts.sessionStale,
520
+ contributeNudge: opts.contributeNudge,
504
521
  seenHistory
505
522
  });
506
523
  if (verbs.length > 0) applySpinnerVerbs(verbs, sc.mode);
@@ -519,6 +536,7 @@ export {
519
536
  applySpinnerTips,
520
537
  applySpinnerVerbs,
521
538
  buildContextVerbs,
539
+ buildContributeNudgeLine,
522
540
  buildIncomingIntroLine,
523
541
  buildPeerLine,
524
542
  buildSessionStaleLine,
@@ -59,9 +59,13 @@ function isInboundNudgeMuted() {
59
59
  function isContributeEnabled() {
60
60
  return readConfig().contributeEnabled === true;
61
61
  }
62
+ function isContributePrompted() {
63
+ return readConfig().contributePrompted === true;
64
+ }
62
65
  export {
63
66
  getNudgeMode,
64
67
  isContributeEnabled,
68
+ isContributePrompted,
65
69
  isInboundNudgeMuted,
66
70
  isPeerConnectEnabled,
67
71
  parseNudgeMode,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminalhire",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "Local-first job matching for developers — ambient job matches in the Claude Code spinner. Matching runs on your machine; your profile never leaves it.",
5
5
  "repository": {
6
6
  "type": "git",