terminalhire 0.42.1 → 0.42.2

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.
@@ -167,7 +167,7 @@ function nowISO() {
167
167
  return (/* @__PURE__ */ new Date()).toISOString();
168
168
  }
169
169
  function defangText(s) {
170
- return typeof s === "string" ? s.replace(CONTROL_CHARS, "") : s;
170
+ return typeof s === "string" ? s.replace(WHITESPACE_CONTROLS, " ").replace(CONTROL_CHARS, "") : s;
171
171
  }
172
172
  function finiteAmount(a) {
173
173
  if (typeof a === "number") return Number.isFinite(a) ? a : null;
@@ -305,7 +305,7 @@ function acceptedPRRate(claims = readClaims()) {
305
305
  const merged = claims.filter((c) => c.state === "merged").length;
306
306
  return { merged, total, rate: total === 0 ? 0 : merged / total };
307
307
  }
308
- var TERMINALHIRE_DIR3, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, CONTROL_CHARS;
308
+ var TERMINALHIRE_DIR3, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, WHITESPACE_CONTROLS, CONTROL_CHARS;
309
309
  var init_claims = __esm({
310
310
  "src/claims.ts"() {
311
311
  "use strict";
@@ -361,6 +361,7 @@ var init_claims = __esm({
361
361
  ]),
362
362
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
363
363
  };
364
+ WHITESPACE_CONTROLS = /[\t\n\v\f\r]+/g;
364
365
  CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
365
366
  }
366
367
  });
@@ -11339,7 +11339,7 @@ function nowISO2() {
11339
11339
  return (/* @__PURE__ */ new Date()).toISOString();
11340
11340
  }
11341
11341
  function defangText(s) {
11342
- return typeof s === "string" ? s.replace(CONTROL_CHARS2, "") : s;
11342
+ return typeof s === "string" ? s.replace(WHITESPACE_CONTROLS2, " ").replace(CONTROL_CHARS2, "") : s;
11343
11343
  }
11344
11344
  function finiteAmount(a) {
11345
11345
  if (typeof a === "number") return Number.isFinite(a) ? a : null;
@@ -11477,7 +11477,7 @@ function acceptedPRRate(claims = readClaims()) {
11477
11477
  const merged = claims.filter((c) => c.state === "merged").length;
11478
11478
  return { merged, total, rate: total === 0 ? 0 : merged / total };
11479
11479
  }
11480
- var TERMINALHIRE_DIR5, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, CONTROL_CHARS2;
11480
+ var TERMINALHIRE_DIR5, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, WHITESPACE_CONTROLS2, CONTROL_CHARS2;
11481
11481
  var init_claims = __esm({
11482
11482
  "src/claims.ts"() {
11483
11483
  "use strict";
@@ -11533,6 +11533,7 @@ var init_claims = __esm({
11533
11533
  ]),
11534
11534
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
11535
11535
  };
11536
+ WHITESPACE_CONTROLS2 = /[\t\n\v\f\r]+/g;
11536
11537
  CONTROL_CHARS2 = /[\x00-\x1f\x7f-\x9f]/g;
11537
11538
  }
11538
11539
  });
@@ -11748,17 +11749,38 @@ var init_founder_paid_badge = __esm({
11748
11749
 
11749
11750
  // bin/jpi-bounties.js
11750
11751
  init_src();
11751
- init_cache_store();
11752
11752
  import { readFileSync as readFileSync7 } from "fs";
11753
11753
  import { join as join10 } from "path";
11754
11754
  import { homedir as homedir7 } from "os";
11755
11755
  import { createInterface } from "readline";
11756
11756
 
11757
+ // ../../packages/core/src/payout-split.ts
11758
+ var PLATFORM_TAKE_BPS = 1e3;
11759
+ function platformFeeCents(totalCents) {
11760
+ return Math.ceil(totalCents * PLATFORM_TAKE_BPS / 1e4);
11761
+ }
11762
+ function developerShareCents(totalCents) {
11763
+ return totalCents - platformFeeCents(totalCents);
11764
+ }
11765
+ function formatCents(cents) {
11766
+ return cents % 100 === 0 ? `$${cents / 100}` : `$${(cents / 100).toFixed(2)}`;
11767
+ }
11768
+
11769
+ // bin/jpi-bounties.js
11770
+ init_cache_store();
11771
+
11757
11772
  // bin/sanitize.js
11773
+ var WHITESPACE_CONTROLS = /[\t\n\v\f\r]+/g;
11758
11774
  var CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
11759
11775
  function sanitizeText(s) {
11760
11776
  if (s == null) return "";
11761
- return String(s).replace(CONTROL_CHARS, "");
11777
+ return String(s).replace(WHITESPACE_CONTROLS, " ").replace(CONTROL_CHARS, "");
11778
+ }
11779
+ function formatUsd(a) {
11780
+ if (typeof a === "number") return Number.isFinite(a) ? "$" + a.toLocaleString() : "$\u2014";
11781
+ if (typeof a !== "string" || a.trim() === "") return "$\u2014";
11782
+ const n = Number(a);
11783
+ return Number.isFinite(n) ? "$" + n.toLocaleString() : "$\u2014";
11762
11784
  }
11763
11785
  function safeHttpUrl(url) {
11764
11786
  if (url == null) return null;
@@ -11831,13 +11853,33 @@ function prompt(question) {
11831
11853
  });
11832
11854
  }
11833
11855
  function formatAmount(b) {
11834
- return b.amountUSD != null ? "$" + b.amountUSD.toLocaleString() : "$\u2014";
11856
+ return formatUsd(b.amountUSD);
11835
11857
  }
11836
11858
  var EFFORT_LABEL = {
11837
11859
  small: "small (~\xBD day)",
11838
11860
  medium: "medium (~1 day)",
11839
11861
  large: "large (multi-day)"
11840
11862
  };
11863
+ function founderClaimBlurb(amountUSD, paidWork) {
11864
+ if (paidWork === false) {
11865
+ return "Claim it from here \u2014 this posting records a credential for the work, not a payment. Take it for the credential, not the money.";
11866
+ }
11867
+ if (paidWork !== true) {
11868
+ return "Claim it from here \u2014 this listing is missing its payment status, so it is not safe to say whether the work pays. Try `terminalhire refresh`; if it is still missing, do not count on being paid for this one.";
11869
+ }
11870
+ let share = "";
11871
+ if (typeof amountUSD === "number" && Number.isFinite(amountUSD) && amountUSD > 0) {
11872
+ share = ` You'd receive ${formatCents(developerShareCents(Math.round(amountUSD * 100)))} of the ${formatCents(Math.round(amountUSD * 100))} posted; terminalhire keeps 10%.`;
11873
+ }
11874
+ return `Claim it from here \u2014 the founder pays through terminalhire rather than off-platform, and their acceptance is what triggers your payout.${share}`;
11875
+ }
11876
+ function founderClaimBlock(job) {
11877
+ const ref = opportunityShortToken(job.id);
11878
+ return `
11879
+ ${founderClaimBlurb(job.bounty?.amountUSD, job.bounty?.paidWork)}
11880
+ terminalhire claim ${ref}
11881
+ ${sanitizeText(job.bounty?.claimUrl ?? job.url)}`;
11882
+ }
11841
11883
  function printBounty(i, job, score, reason, matchedTags, claimedIds = /* @__PURE__ */ new Set(), continuityNote = null) {
11842
11884
  const b = job.bounty ?? {};
11843
11885
  const stars = b.repoStars != null ? ` \xB7 ${b.repoStars}\u2605` : "";
@@ -11980,7 +12022,9 @@ async function run() {
11980
12022
  const rows = founderRows2(surface, "bounties");
11981
12023
  console.log("\n\u26A1 Your TerminalHire postings\n");
11982
12024
  if (!rows.length) {
11983
- console.log(" No current posting data. Run `terminalhire refresh`, or lead with work:");
12025
+ console.log(
12026
+ " No current posting data. Run `terminalhire refresh`, or lead with work:"
12027
+ );
11984
12028
  console.log(" terminalhire config set lead dev\n");
11985
12029
  } else {
11986
12030
  rows.forEach((row, index) => {
@@ -11989,7 +12033,9 @@ async function run() {
11989
12033
  );
11990
12034
  });
11991
12035
  console.log("\n Review and act: https://terminalhire.com/dashboard?tab=postings");
11992
- console.log(" Lead with developer bounties instead: terminalhire config set lead dev\n");
12036
+ console.log(
12037
+ " Lead with developer bounties instead: terminalhire config set lead dev\n"
12038
+ );
11993
12039
  }
11994
12040
  return;
11995
12041
  }
@@ -12064,6 +12110,10 @@ Enter a number to open a bounty's claim page, or press Enter to exit: `
12064
12110
  const idx = parseInt(pick, 10) - 1;
12065
12111
  if (Number.isNaN(idx) || idx < 0 || idx >= shown.length) return;
12066
12112
  const chosen = shown[idx];
12113
+ if (chosen.bounty?.bountySource === "founder") {
12114
+ console.log(founderClaimBlock(chosen));
12115
+ return;
12116
+ }
12067
12117
  console.log(
12068
12118
  `
12069
12119
  Open this to claim/work the bounty (you go straight to the source \u2014 we never touch payment):
@@ -12079,6 +12129,8 @@ export {
12079
12129
  classifyEmptyStatus,
12080
12130
  continuityNoteForRow,
12081
12131
  filterPaidVisibility,
12132
+ founderClaimBlock,
12133
+ founderClaimBlurb,
12082
12134
  getBounties,
12083
12135
  isPinnedFounderBounty,
12084
12136
  printBounty,
@@ -10639,7 +10639,7 @@ function nowISO() {
10639
10639
  return (/* @__PURE__ */ new Date()).toISOString();
10640
10640
  }
10641
10641
  function defangText(s) {
10642
- return typeof s === "string" ? s.replace(CONTROL_CHARS2, "") : s;
10642
+ return typeof s === "string" ? s.replace(WHITESPACE_CONTROLS2, " ").replace(CONTROL_CHARS2, "") : s;
10643
10643
  }
10644
10644
  function finiteAmount(a) {
10645
10645
  if (typeof a === "number") return Number.isFinite(a) ? a : null;
@@ -10777,7 +10777,7 @@ function acceptedPRRate(claims = readClaims()) {
10777
10777
  const merged = claims.filter((c) => c.state === "merged").length;
10778
10778
  return { merged, total, rate: total === 0 ? 0 : merged / total };
10779
10779
  }
10780
- var TERMINALHIRE_DIR2, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, CONTROL_CHARS2;
10780
+ var TERMINALHIRE_DIR2, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, WHITESPACE_CONTROLS2, CONTROL_CHARS2;
10781
10781
  var init_claims = __esm({
10782
10782
  "src/claims.ts"() {
10783
10783
  "use strict";
@@ -10833,6 +10833,7 @@ var init_claims = __esm({
10833
10833
  ]),
10834
10834
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
10835
10835
  };
10836
+ WHITESPACE_CONTROLS2 = /[\t\n\v\f\r]+/g;
10836
10837
  CONTROL_CHARS2 = /[\x00-\x1f\x7f-\x9f]/g;
10837
10838
  }
10838
10839
  });
@@ -26151,10 +26152,17 @@ function openInBrowser(url) {
26151
26152
  }
26152
26153
 
26153
26154
  // bin/sanitize.js
26155
+ var WHITESPACE_CONTROLS = /[\t\n\v\f\r]+/g;
26154
26156
  var CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
26155
26157
  function sanitizeText(s) {
26156
26158
  if (s == null) return "";
26157
- return String(s).replace(CONTROL_CHARS, "");
26159
+ return String(s).replace(WHITESPACE_CONTROLS, " ").replace(CONTROL_CHARS, "");
26160
+ }
26161
+ function formatUsd(a) {
26162
+ if (typeof a === "number") return Number.isFinite(a) ? "$" + a.toLocaleString() : "$\u2014";
26163
+ if (typeof a !== "string" || a.trim() === "") return "$\u2014";
26164
+ const n = Number(a);
26165
+ return Number.isFinite(n) ? "$" + n.toLocaleString() : "$\u2014";
26158
26166
  }
26159
26167
 
26160
26168
  // src/policy-acks.ts
@@ -27081,10 +27089,7 @@ async function pollPR(prUrl) {
27081
27089
  }
27082
27090
  }
27083
27091
  function fmtAmount(a) {
27084
- if (a == null) return "$\u2014";
27085
- if (typeof a === "string" && a.trim() === "") return "$\u2014";
27086
- const n = typeof a === "number" ? a : Number(a);
27087
- return Number.isFinite(n) ? "$" + n.toLocaleString() : "$\u2014";
27092
+ return formatUsd(a);
27088
27093
  }
27089
27094
  function fmtClaimAmount(c) {
27090
27095
  return c.kind === "contribution" ? "contribution" : fmtAmount(c.amountUSD);
@@ -27225,8 +27230,15 @@ async function resolveBounty(arg) {
27225
27230
  return {
27226
27231
  bountyId,
27227
27232
  indexNativeId,
27228
- title,
27229
- repoFullName,
27233
+ // DEFANGED HERE, at the one place every tier of this resolver converges.
27234
+ // `claims.ts` cleans what comes OUT of the local store, which covers every later
27235
+ // read — but this object is printed (the preview card, the record card) and
27236
+ // handed to `recordClaim` BEFORE any store exists for it, so on a claim's first
27237
+ // use the store boundary has not run yet. `repoFullName` also goes on to be
27238
+ // interpolated into GitHub API URLs from here. Found by a cross-model review of
27239
+ // TERM-380; the store fix alone left this path open.
27240
+ title: sanitizeText(title),
27241
+ repoFullName: repoFullName == null ? repoFullName : sanitizeText(repoFullName),
27230
27242
  issueUrl,
27231
27243
  amountUSD,
27232
27244
  source,
@@ -28466,7 +28478,7 @@ async function cmdStart(id, flags = {}) {
28466
28478
  console.error(`terminalhire claim: no claim with id '${id}'.`);
28467
28479
  process.exit(1);
28468
28480
  }
28469
- if (claim.approval?.state === "pending" && !approvalsChecked) {
28481
+ if (claim.approval?.state === "pending") {
28470
28482
  ({ approvalsChecked, approvalsUnavailable } = await syncFounderApprovals(claims, [claim]));
28471
28483
  claim = claims.findClaim(id);
28472
28484
  }
@@ -3618,7 +3618,7 @@ function nowISO2() {
3618
3618
  return (/* @__PURE__ */ new Date()).toISOString();
3619
3619
  }
3620
3620
  function defangText(s) {
3621
- return typeof s === "string" ? s.replace(CONTROL_CHARS2, "") : s;
3621
+ return typeof s === "string" ? s.replace(WHITESPACE_CONTROLS2, " ").replace(CONTROL_CHARS2, "") : s;
3622
3622
  }
3623
3623
  function finiteAmount(a) {
3624
3624
  if (typeof a === "number") return Number.isFinite(a) ? a : null;
@@ -3756,7 +3756,7 @@ function acceptedPRRate(claims = readClaims()) {
3756
3756
  const merged = claims.filter((c) => c.state === "merged").length;
3757
3757
  return { merged, total, rate: total === 0 ? 0 : merged / total };
3758
3758
  }
3759
- var TERMINALHIRE_DIR7, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, CONTROL_CHARS2;
3759
+ var TERMINALHIRE_DIR7, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, WHITESPACE_CONTROLS2, CONTROL_CHARS2;
3760
3760
  var init_claims = __esm({
3761
3761
  "src/claims.ts"() {
3762
3762
  "use strict";
@@ -3812,6 +3812,7 @@ var init_claims = __esm({
3812
3812
  ]),
3813
3813
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
3814
3814
  };
3815
+ WHITESPACE_CONTROLS2 = /[\t\n\v\f\r]+/g;
3815
3816
  CONTROL_CHARS2 = /[\x00-\x1f\x7f-\x9f]/g;
3816
3817
  }
3817
3818
  });
@@ -3896,10 +3897,11 @@ function isContributeEnabled() {
3896
3897
  init_state_dir();
3897
3898
 
3898
3899
  // bin/sanitize.js
3900
+ var WHITESPACE_CONTROLS = /[\t\n\v\f\r]+/g;
3899
3901
  var CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
3900
3902
  function sanitizeText(s) {
3901
3903
  if (s == null) return "";
3902
- return String(s).replace(CONTROL_CHARS, "");
3904
+ return String(s).replace(WHITESPACE_CONTROLS, " ").replace(CONTROL_CHARS, "");
3903
3905
  }
3904
3906
  function safeHttpUrl(url) {
3905
3907
  if (url == null) return null;
@@ -11053,10 +11053,11 @@ function excludeOwnCard(results, ownLogin) {
11053
11053
  }
11054
11054
 
11055
11055
  // bin/sanitize.js
11056
+ var WHITESPACE_CONTROLS = /[\t\n\v\f\r]+/g;
11056
11057
  var CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
11057
11058
  function sanitizeText(s) {
11058
11059
  if (s == null) return "";
11059
- return String(s).replace(CONTROL_CHARS, "");
11060
+ return String(s).replace(WHITESPACE_CONTROLS, " ").replace(CONTROL_CHARS, "");
11060
11061
  }
11061
11062
  function safeHttpUrl(url) {
11062
11063
  if (url == null) return null;
@@ -12831,7 +12831,13 @@ var init_cache_store = __esm({
12831
12831
  // bin/sanitize.js
12832
12832
  function sanitizeText(s) {
12833
12833
  if (s == null) return "";
12834
- return String(s).replace(CONTROL_CHARS, "");
12834
+ return String(s).replace(WHITESPACE_CONTROLS, " ").replace(CONTROL_CHARS, "");
12835
+ }
12836
+ function formatUsd(a) {
12837
+ if (typeof a === "number") return Number.isFinite(a) ? "$" + a.toLocaleString() : "$\u2014";
12838
+ if (typeof a !== "string" || a.trim() === "") return "$\u2014";
12839
+ const n = Number(a);
12840
+ return Number.isFinite(n) ? "$" + n.toLocaleString() : "$\u2014";
12835
12841
  }
12836
12842
  function safeHttpUrl(url) {
12837
12843
  if (url == null) return null;
@@ -12857,10 +12863,11 @@ function linkTitle(title, url) {
12857
12863
  }
12858
12864
  return href ? `${safeTitle} (${href})` : safeTitle;
12859
12865
  }
12860
- var CONTROL_CHARS;
12866
+ var WHITESPACE_CONTROLS, CONTROL_CHARS;
12861
12867
  var init_sanitize = __esm({
12862
12868
  "bin/sanitize.js"() {
12863
12869
  "use strict";
12870
+ WHITESPACE_CONTROLS = /[\t\n\v\f\r]+/g;
12864
12871
  CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
12865
12872
  }
12866
12873
  });
@@ -13578,7 +13585,7 @@ function buildTipsDetailed(topMatches, baseUrl, max = 8, opts = {}) {
13578
13585
  const token = jobShortToken(String(m.id));
13579
13586
  const url = `${base}/j/${token}`;
13580
13587
  if (source === "bounty") {
13581
- const money = m.amountUSD != null ? `$${Number(m.amountUSD).toLocaleString()}` : "$\u2014";
13588
+ const money = formatUsd(m.amountUSD);
13582
13589
  const repo = m.repo || companyRaw;
13583
13590
  out.push(`\u{1F48E} ${money} \xB7 ${title} \xB7 ${repo} \xB7 ${pct2}% \u2014 ${url}`);
13584
13591
  } else if (source === "contribute") {
@@ -13648,6 +13655,7 @@ var init_spinner_render = __esm({
13648
13655
  init_spinner_verbs();
13649
13656
  init_spinner_seen();
13650
13657
  init_spinner_io();
13658
+ init_sanitize();
13651
13659
  init_src();
13652
13660
  }
13653
13661
  });
@@ -14853,6 +14861,24 @@ var init_jpi_project = __esm({
14853
14861
  }
14854
14862
  });
14855
14863
 
14864
+ // ../../packages/core/src/payout-split.ts
14865
+ function platformFeeCents(totalCents) {
14866
+ return Math.ceil(totalCents * PLATFORM_TAKE_BPS / 1e4);
14867
+ }
14868
+ function developerShareCents(totalCents) {
14869
+ return totalCents - platformFeeCents(totalCents);
14870
+ }
14871
+ function formatCents(cents) {
14872
+ return cents % 100 === 0 ? `$${cents / 100}` : `$${(cents / 100).toFixed(2)}`;
14873
+ }
14874
+ var PLATFORM_TAKE_BPS;
14875
+ var init_payout_split = __esm({
14876
+ "../../packages/core/src/payout-split.ts"() {
14877
+ "use strict";
14878
+ PLATFORM_TAKE_BPS = 1e3;
14879
+ }
14880
+ });
14881
+
14856
14882
  // bin/founder-pin.js
14857
14883
  function isPinnedFounderBounty(j) {
14858
14884
  return j?.bounty?.bountySource === "founder" && j?.bounty?.claimable === true;
@@ -15182,7 +15208,7 @@ function nowISO2() {
15182
15208
  return (/* @__PURE__ */ new Date()).toISOString();
15183
15209
  }
15184
15210
  function defangText(s) {
15185
- return typeof s === "string" ? s.replace(CONTROL_CHARS2, "") : s;
15211
+ return typeof s === "string" ? s.replace(WHITESPACE_CONTROLS2, " ").replace(CONTROL_CHARS2, "") : s;
15186
15212
  }
15187
15213
  function finiteAmount(a) {
15188
15214
  if (typeof a === "number") return Number.isFinite(a) ? a : null;
@@ -15320,7 +15346,7 @@ function acceptedPRRate(claims = readClaims()) {
15320
15346
  const merged = claims.filter((c) => c.state === "merged").length;
15321
15347
  return { merged, total, rate: total === 0 ? 0 : merged / total };
15322
15348
  }
15323
- var TERMINALHIRE_DIR11, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS2, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, CONTROL_CHARS2;
15349
+ var TERMINALHIRE_DIR11, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS2, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, WHITESPACE_CONTROLS2, CONTROL_CHARS2;
15324
15350
  var init_claims = __esm({
15325
15351
  "src/claims.ts"() {
15326
15352
  "use strict";
@@ -15376,6 +15402,7 @@ var init_claims = __esm({
15376
15402
  ]),
15377
15403
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
15378
15404
  };
15405
+ WHITESPACE_CONTROLS2 = /[\t\n\v\f\r]+/g;
15379
15406
  CONTROL_CHARS2 = /[\x00-\x1f\x7f-\x9f]/g;
15380
15407
  }
15381
15408
  });
@@ -15424,6 +15451,8 @@ __export(jpi_bounties_exports, {
15424
15451
  classifyEmptyStatus: () => classifyEmptyStatus,
15425
15452
  continuityNoteForRow: () => continuityNoteForRow,
15426
15453
  filterPaidVisibility: () => filterPaidVisibility,
15454
+ founderClaimBlock: () => founderClaimBlock,
15455
+ founderClaimBlurb: () => founderClaimBlurb,
15427
15456
  getBounties: () => getBounties,
15428
15457
  isPinnedFounderBounty: () => isPinnedFounderBounty,
15429
15458
  printBounty: () => printBounty,
@@ -15465,7 +15494,27 @@ function prompt3(question) {
15465
15494
  });
15466
15495
  }
15467
15496
  function formatAmount(b) {
15468
- return b.amountUSD != null ? "$" + b.amountUSD.toLocaleString() : "$\u2014";
15497
+ return formatUsd(b.amountUSD);
15498
+ }
15499
+ function founderClaimBlurb(amountUSD, paidWork) {
15500
+ if (paidWork === false) {
15501
+ return "Claim it from here \u2014 this posting records a credential for the work, not a payment. Take it for the credential, not the money.";
15502
+ }
15503
+ if (paidWork !== true) {
15504
+ return "Claim it from here \u2014 this listing is missing its payment status, so it is not safe to say whether the work pays. Try `terminalhire refresh`; if it is still missing, do not count on being paid for this one.";
15505
+ }
15506
+ let share = "";
15507
+ if (typeof amountUSD === "number" && Number.isFinite(amountUSD) && amountUSD > 0) {
15508
+ share = ` You'd receive ${formatCents(developerShareCents(Math.round(amountUSD * 100)))} of the ${formatCents(Math.round(amountUSD * 100))} posted; terminalhire keeps 10%.`;
15509
+ }
15510
+ return `Claim it from here \u2014 the founder pays through terminalhire rather than off-platform, and their acceptance is what triggers your payout.${share}`;
15511
+ }
15512
+ function founderClaimBlock(job) {
15513
+ const ref = opportunityShortToken(job.id);
15514
+ return `
15515
+ ${founderClaimBlurb(job.bounty?.amountUSD, job.bounty?.paidWork)}
15516
+ terminalhire claim ${ref}
15517
+ ${sanitizeText(job.bounty?.claimUrl ?? job.url)}`;
15469
15518
  }
15470
15519
  function printBounty(i, job, score, reason, matchedTags, claimedIds = /* @__PURE__ */ new Set(), continuityNote = null) {
15471
15520
  const b = job.bounty ?? {};
@@ -15609,7 +15658,9 @@ async function run5() {
15609
15658
  const rows = founderRows2(surface, "bounties");
15610
15659
  console.log("\n\u26A1 Your TerminalHire postings\n");
15611
15660
  if (!rows.length) {
15612
- console.log(" No current posting data. Run `terminalhire refresh`, or lead with work:");
15661
+ console.log(
15662
+ " No current posting data. Run `terminalhire refresh`, or lead with work:"
15663
+ );
15613
15664
  console.log(" terminalhire config set lead dev\n");
15614
15665
  } else {
15615
15666
  rows.forEach((row, index) => {
@@ -15618,7 +15669,9 @@ async function run5() {
15618
15669
  );
15619
15670
  });
15620
15671
  console.log("\n Review and act: https://terminalhire.com/dashboard?tab=postings");
15621
- console.log(" Lead with developer bounties instead: terminalhire config set lead dev\n");
15672
+ console.log(
15673
+ " Lead with developer bounties instead: terminalhire config set lead dev\n"
15674
+ );
15622
15675
  }
15623
15676
  return;
15624
15677
  }
@@ -15693,6 +15746,10 @@ Enter a number to open a bounty's claim page, or press Enter to exit: `
15693
15746
  const idx = parseInt(pick2, 10) - 1;
15694
15747
  if (Number.isNaN(idx) || idx < 0 || idx >= shown.length) return;
15695
15748
  const chosen = shown[idx];
15749
+ if (chosen.bounty?.bountySource === "founder") {
15750
+ console.log(founderClaimBlock(chosen));
15751
+ return;
15752
+ }
15696
15753
  console.log(
15697
15754
  `
15698
15755
  Open this to claim/work the bounty (you go straight to the source \u2014 we never touch payment):
@@ -15708,6 +15765,7 @@ var init_jpi_bounties = __esm({
15708
15765
  "bin/jpi-bounties.js"() {
15709
15766
  "use strict";
15710
15767
  init_src();
15768
+ init_payout_split();
15711
15769
  init_cache_store();
15712
15770
  init_sanitize();
15713
15771
  init_founder_pin();
@@ -31679,10 +31737,7 @@ async function pollPR(prUrl) {
31679
31737
  }
31680
31738
  }
31681
31739
  function fmtAmount(a) {
31682
- if (a == null) return "$\u2014";
31683
- if (typeof a === "string" && a.trim() === "") return "$\u2014";
31684
- const n = typeof a === "number" ? a : Number(a);
31685
- return Number.isFinite(n) ? "$" + n.toLocaleString() : "$\u2014";
31740
+ return formatUsd(a);
31686
31741
  }
31687
31742
  function fmtClaimAmount(c) {
31688
31743
  return c.kind === "contribution" ? "contribution" : fmtAmount(c.amountUSD);
@@ -31821,8 +31876,15 @@ async function resolveBounty(arg) {
31821
31876
  return {
31822
31877
  bountyId,
31823
31878
  indexNativeId,
31824
- title,
31825
- repoFullName,
31879
+ // DEFANGED HERE, at the one place every tier of this resolver converges.
31880
+ // `claims.ts` cleans what comes OUT of the local store, which covers every later
31881
+ // read — but this object is printed (the preview card, the record card) and
31882
+ // handed to `recordClaim` BEFORE any store exists for it, so on a claim's first
31883
+ // use the store boundary has not run yet. `repoFullName` also goes on to be
31884
+ // interpolated into GitHub API URLs from here. Found by a cross-model review of
31885
+ // TERM-380; the store fix alone left this path open.
31886
+ title: sanitizeText(title),
31887
+ repoFullName: repoFullName == null ? repoFullName : sanitizeText(repoFullName),
31826
31888
  issueUrl,
31827
31889
  amountUSD,
31828
31890
  source,
@@ -33043,7 +33105,7 @@ async function cmdStart(id, flags = {}) {
33043
33105
  console.error(`terminalhire claim: no claim with id '${id}'.`);
33044
33106
  process.exit(1);
33045
33107
  }
33046
- if (claim.approval?.state === "pending" && !approvalsChecked) {
33108
+ if (claim.approval?.state === "pending") {
33047
33109
  ({ approvalsChecked, approvalsUnavailable } = await syncFounderApprovals(claims, [claim]));
33048
33110
  claim = claims.findClaim(id);
33049
33111
  }
@@ -39891,7 +39953,12 @@ function substrMatch(query, candidate) {
39891
39953
  if (query === "") return true;
39892
39954
  return candidate.toLowerCase().includes(query.toLowerCase());
39893
39955
  }
39894
- function runHubTui({ input = process.stdin, output = process.stdout, signals, deps = {} } = {}) {
39956
+ function runHubTui({
39957
+ input = process.stdin,
39958
+ output = process.stdout,
39959
+ signals,
39960
+ deps = {}
39961
+ } = {}) {
39895
39962
  const {
39896
39963
  listClaims: _listClaims = listClaims,
39897
39964
  acceptedPRRate: _acceptedPRRate = acceptedPRRate,
@@ -40054,20 +40121,50 @@ function runHubTui({ input = process.stdin, output = process.stdout, signals, de
40054
40121
  acked = false;
40055
40122
  }
40056
40123
  if (!acked) {
40057
- inboxState = { loaded: true, loading: false, status: "disclosure", items: null, message: null };
40124
+ inboxState = {
40125
+ loaded: true,
40126
+ loading: false,
40127
+ status: "disclosure",
40128
+ items: null,
40129
+ message: null
40130
+ };
40058
40131
  return;
40059
40132
  }
40060
40133
  inboxState = { ...inboxState, loading: true };
40061
40134
  _buildInboxItems().then((built) => {
40062
40135
  if (built.status === "ok") {
40063
- inboxState = { loaded: true, loading: false, status: "ok", items: built.items, message: null };
40136
+ inboxState = {
40137
+ loaded: true,
40138
+ loading: false,
40139
+ status: "ok",
40140
+ items: built.items,
40141
+ message: null
40142
+ };
40064
40143
  } else if (built.status === "error") {
40065
- inboxState = { loaded: true, loading: false, status: "error", items: null, message: built.message || "could not load inbox" };
40144
+ inboxState = {
40145
+ loaded: true,
40146
+ loading: false,
40147
+ status: "error",
40148
+ items: null,
40149
+ message: built.message || "could not load inbox"
40150
+ };
40066
40151
  } else {
40067
- inboxState = { loaded: true, loading: false, status: built.status, items: null, message: null };
40152
+ inboxState = {
40153
+ loaded: true,
40154
+ loading: false,
40155
+ status: built.status,
40156
+ items: null,
40157
+ message: null
40158
+ };
40068
40159
  }
40069
40160
  }).catch((e) => {
40070
- inboxState = { loaded: true, loading: false, status: "error", items: null, message: errMsg(e) };
40161
+ inboxState = {
40162
+ loaded: true,
40163
+ loading: false,
40164
+ status: "error",
40165
+ items: null,
40166
+ message: errMsg(e)
40167
+ };
40071
40168
  }).finally(() => repaint());
40072
40169
  return;
40073
40170
  }
@@ -40101,7 +40198,9 @@ function runHubTui({ input = process.stdin, output = process.stdout, signals, de
40101
40198
  function inboxRows(st) {
40102
40199
  switch (st.status) {
40103
40200
  case "disclosure":
40104
- return ["Messaging disclosure not yet acknowledged \u2014 run `terminalhire inbox` once to review it and enable messages."];
40201
+ return [
40202
+ "Messaging disclosure not yet acknowledged \u2014 run `terminalhire inbox` once to review it and enable messages."
40203
+ ];
40105
40204
  case "not-linked":
40106
40205
  return ["Not linked \u2014 run `terminalhire link` to connect your web session."];
40107
40206
  case "expired":
@@ -40144,11 +40243,13 @@ function runHubTui({ input = process.stdin, output = process.stdout, signals, de
40144
40243
  case "empty":
40145
40244
  return ["No bounties available right now. Check back through the day."];
40146
40245
  case "demoted":
40147
- return ["Paid bounties paused \u2014 run `bounties --priced` to view them. Try Contribute or Jobs."];
40246
+ return [
40247
+ "Paid bounties paused \u2014 run `bounties --priced` to view them. Try Contribute or Jobs."
40248
+ ];
40148
40249
  case "ok":
40149
40250
  return result.bounties.map((job) => {
40150
40251
  const b = job.bounty || {};
40151
- const amt = b.amountUSD != null ? "$" + b.amountUSD.toLocaleString() : "$\u2014";
40252
+ const amt = formatUsd(b.amountUSD);
40152
40253
  const repo = sanitizeLine(b.repoFullName || job.company);
40153
40254
  const prs = b.competingOpenPRs;
40154
40255
  const contend = prs != null && prs > 0 ? ` \xB7 \u26A0 ${prs} in flight` : "";
@@ -40203,7 +40304,9 @@ function runHubTui({ input = process.stdin, output = process.stdout, signals, de
40203
40304
  if (it.note) rows.push(` note: ${sanitizeLine(it.note)}`);
40204
40305
  if (it.contact) rows.push(` contact: ${sanitizeLine(it.contact)}`);
40205
40306
  else if (it.role === "incoming" && it.status === "pending") {
40206
- rows.push(` \u2192 accept: terminalhire intro --accept @${sanitizeLine(it.counterpartyLogin)}`);
40307
+ rows.push(
40308
+ ` \u2192 accept: terminalhire intro --accept @${sanitizeLine(it.counterpartyLogin)}`
40309
+ );
40207
40310
  }
40208
40311
  }
40209
40312
  return rows;
@@ -40290,7 +40393,8 @@ function runHubTui({ input = process.stdin, output = process.stdout, signals, de
40290
40393
  if (name === "Claims") {
40291
40394
  if (!claimsState.loaded) return ["Loading\u2026"];
40292
40395
  if (claimsState.error) return ["Could not read claims \u2014 " + claimsState.error];
40293
- if (!claimsState.rows.length) return ["No claims yet \u2014 run `terminalhire claim <url>` to start one."];
40396
+ if (!claimsState.rows.length)
40397
+ return ["No claims yet \u2014 run `terminalhire claim <url>` to start one."];
40294
40398
  return claimsState.rows.map(formatClaimRow);
40295
40399
  }
40296
40400
  if (name === "Profile") {
@@ -40320,8 +40424,10 @@ function runHubTui({ input = process.stdin, output = process.stdout, signals, de
40320
40424
  const st = listState[name];
40321
40425
  if (st.loaded && !st.error && st.result && st.result.status === "ok") {
40322
40426
  if (name === "Jobs") return `${st.result.ranked.length} roles matching your profile`;
40323
- if (name === "Bounties") return `${st.result.bounties.length} bounties you could knock out`;
40324
- if (name === "Devs") return `${st.result.results.length} matches in the builder directory`;
40427
+ if (name === "Bounties")
40428
+ return `${st.result.bounties.length} bounties you could knock out`;
40429
+ if (name === "Devs")
40430
+ return `${st.result.results.length} matches in the builder directory`;
40325
40431
  }
40326
40432
  return name;
40327
40433
  }
@@ -40351,7 +40457,9 @@ function runHubTui({ input = process.stdin, output = process.stdout, signals, de
40351
40457
  const row = startRow + i;
40352
40458
  if (row < 0 || row >= rows) continue;
40353
40459
  const t = TH_GLYPH.length <= 1 ? 0 : i / (TH_GLYPH.length - 1);
40354
- drawText(buf, cols, row, startCol, TH_GLYPH[i], { fg: gradientFg(BRAND_GRADIENT, t, level) });
40460
+ drawText(buf, cols, row, startCol, TH_GLYPH[i], {
40461
+ fg: gradientFg(BRAND_GRADIENT, t, level)
40462
+ });
40355
40463
  }
40356
40464
  const hint = "press any key to continue";
40357
40465
  const hintRow = Math.min(rows - 1, startRow + TH_GLYPH.length + 1);
@@ -40805,6 +40913,7 @@ var init_jpi_hub = __esm({
40805
40913
  "bin/jpi-hub.js"() {
40806
40914
  "use strict";
40807
40915
  init_tui_core();
40916
+ init_sanitize();
40808
40917
  init_claims();
40809
40918
  init_profile();
40810
40919
  init_config();
@@ -40842,7 +40951,15 @@ var init_jpi_hub = __esm({
40842
40951
  ];
40843
40952
  BRAND_GRADIENT = ["#9d8fff", "#7c6af7", "#5b4fcf"];
40844
40953
  DEFAULT_SPLASH_MS = 900;
40845
- STATE_LEVEL = { abandoned: 0, claimed: 2, working: 3, "in-review": 4, ready: 5, submitted: 6, merged: 7 };
40954
+ STATE_LEVEL = {
40955
+ abandoned: 0,
40956
+ claimed: 2,
40957
+ working: 3,
40958
+ "in-review": 4,
40959
+ ready: 5,
40960
+ submitted: 6,
40961
+ merged: 7
40962
+ };
40846
40963
  SPARK_LEVELS = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
40847
40964
  }
40848
40965
  });
@@ -380,6 +380,19 @@ var init_tui_core = __esm({
380
380
  }
381
381
  });
382
382
 
383
+ // bin/sanitize.js
384
+ function formatUsd(a) {
385
+ if (typeof a === "number") return Number.isFinite(a) ? "$" + a.toLocaleString() : "$\u2014";
386
+ if (typeof a !== "string" || a.trim() === "") return "$\u2014";
387
+ const n = Number(a);
388
+ return Number.isFinite(n) ? "$" + n.toLocaleString() : "$\u2014";
389
+ }
390
+ var init_sanitize = __esm({
391
+ "bin/sanitize.js"() {
392
+ "use strict";
393
+ }
394
+ });
395
+
383
396
  // src/state-dir.ts
384
397
  import { closeSync, constants, fchmodSync, fstatSync, mkdirSync, openSync } from "fs";
385
398
  function warnStateDirOnce(dir, message) {
@@ -539,7 +552,7 @@ function nowISO() {
539
552
  return (/* @__PURE__ */ new Date()).toISOString();
540
553
  }
541
554
  function defangText(s) {
542
- return typeof s === "string" ? s.replace(CONTROL_CHARS, "") : s;
555
+ return typeof s === "string" ? s.replace(WHITESPACE_CONTROLS, " ").replace(CONTROL_CHARS, "") : s;
543
556
  }
544
557
  function finiteAmount(a) {
545
558
  if (typeof a === "number") return Number.isFinite(a) ? a : null;
@@ -677,7 +690,7 @@ function acceptedPRRate(claims = readClaims()) {
677
690
  const merged = claims.filter((c) => c.state === "merged").length;
678
691
  return { merged, total, rate: total === 0 ? 0 : merged / total };
679
692
  }
680
- var TERMINALHIRE_DIR, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, CONTROL_CHARS;
693
+ var TERMINALHIRE_DIR, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, WHITESPACE_CONTROLS, CONTROL_CHARS;
681
694
  var init_claims = __esm({
682
695
  "src/claims.ts"() {
683
696
  "use strict";
@@ -733,6 +746,7 @@ var init_claims = __esm({
733
746
  ]),
734
747
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
735
748
  };
749
+ WHITESPACE_CONTROLS = /[\t\n\v\f\r]+/g;
736
750
  CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
737
751
  }
738
752
  });
@@ -12649,6 +12663,7 @@ var init_intro2 = __esm({
12649
12663
 
12650
12664
  // bin/jpi-hub.js
12651
12665
  init_tui_core();
12666
+ init_sanitize();
12652
12667
  init_claims();
12653
12668
  init_profile();
12654
12669
  init_config();
@@ -12685,6 +12700,7 @@ var BAK_FILE = `${STATUS_FILE}.bak`;
12685
12700
 
12686
12701
  // bin/jpi-jobs.js
12687
12702
  init_cache_store();
12703
+ init_sanitize();
12688
12704
  var __dirname = fileURLToPath2(new URL(".", import.meta.url));
12689
12705
  var TERMINALHIRE_DIR11 = process.env.TERMINALHIRE_DIR || join16(homedir13(), ".terminalhire");
12690
12706
  var INDEX_CACHE_FILE3 = join16(TERMINALHIRE_DIR11, "index-cache.json");
@@ -12771,11 +12787,12 @@ async function getJobMatches({ quiet = false, offline = false } = {}) {
12771
12787
 
12772
12788
  // bin/jpi-bounties.js
12773
12789
  init_src();
12774
- init_cache_store();
12775
12790
  import { readFileSync as readFileSync15 } from "fs";
12776
12791
  import { join as join18 } from "path";
12777
12792
  import { homedir as homedir15 } from "os";
12778
12793
  import { createInterface as createInterface3 } from "readline";
12794
+ init_cache_store();
12795
+ init_sanitize();
12779
12796
  init_founder_pin();
12780
12797
  var TERMINALHIRE_DIR13 = process.env.TERMINALHIRE_DIR || join18(homedir15(), ".terminalhire");
12781
12798
  var INDEX_CACHE_FILE4 = join18(TERMINALHIRE_DIR13, "index-cache.json");
@@ -12970,6 +12987,9 @@ function excludeOwnCard(results, ownLogin) {
12970
12987
  });
12971
12988
  }
12972
12989
 
12990
+ // bin/jpi-devs.js
12991
+ init_sanitize();
12992
+
12973
12993
  // bin/match-slots.js
12974
12994
  var ROTATE_WINDOW_MS = 5 * 60 * 1e3;
12975
12995
  var ROLE_HEAD_ROTATE_MS = 60 * 60 * 1e3;
@@ -13081,7 +13101,15 @@ var TH_GLYPH = [
13081
13101
  ];
13082
13102
  var BRAND_GRADIENT = ["#9d8fff", "#7c6af7", "#5b4fcf"];
13083
13103
  var DEFAULT_SPLASH_MS = 900;
13084
- var STATE_LEVEL = { abandoned: 0, claimed: 2, working: 3, "in-review": 4, ready: 5, submitted: 6, merged: 7 };
13104
+ var STATE_LEVEL = {
13105
+ abandoned: 0,
13106
+ claimed: 2,
13107
+ working: 3,
13108
+ "in-review": 4,
13109
+ ready: 5,
13110
+ submitted: 6,
13111
+ merged: 7
13112
+ };
13085
13113
  var SPARK_LEVELS = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
13086
13114
  function sparkline(rows) {
13087
13115
  if (!rows || !rows.length) return "";
@@ -13103,7 +13131,12 @@ function substrMatch(query, candidate) {
13103
13131
  if (query === "") return true;
13104
13132
  return candidate.toLowerCase().includes(query.toLowerCase());
13105
13133
  }
13106
- function runHubTui({ input = process.stdin, output = process.stdout, signals, deps = {} } = {}) {
13134
+ function runHubTui({
13135
+ input = process.stdin,
13136
+ output = process.stdout,
13137
+ signals,
13138
+ deps = {}
13139
+ } = {}) {
13107
13140
  const {
13108
13141
  listClaims: _listClaims = listClaims,
13109
13142
  acceptedPRRate: _acceptedPRRate = acceptedPRRate,
@@ -13266,20 +13299,50 @@ function runHubTui({ input = process.stdin, output = process.stdout, signals, de
13266
13299
  acked = false;
13267
13300
  }
13268
13301
  if (!acked) {
13269
- inboxState = { loaded: true, loading: false, status: "disclosure", items: null, message: null };
13302
+ inboxState = {
13303
+ loaded: true,
13304
+ loading: false,
13305
+ status: "disclosure",
13306
+ items: null,
13307
+ message: null
13308
+ };
13270
13309
  return;
13271
13310
  }
13272
13311
  inboxState = { ...inboxState, loading: true };
13273
13312
  _buildInboxItems().then((built) => {
13274
13313
  if (built.status === "ok") {
13275
- inboxState = { loaded: true, loading: false, status: "ok", items: built.items, message: null };
13314
+ inboxState = {
13315
+ loaded: true,
13316
+ loading: false,
13317
+ status: "ok",
13318
+ items: built.items,
13319
+ message: null
13320
+ };
13276
13321
  } else if (built.status === "error") {
13277
- inboxState = { loaded: true, loading: false, status: "error", items: null, message: built.message || "could not load inbox" };
13322
+ inboxState = {
13323
+ loaded: true,
13324
+ loading: false,
13325
+ status: "error",
13326
+ items: null,
13327
+ message: built.message || "could not load inbox"
13328
+ };
13278
13329
  } else {
13279
- inboxState = { loaded: true, loading: false, status: built.status, items: null, message: null };
13330
+ inboxState = {
13331
+ loaded: true,
13332
+ loading: false,
13333
+ status: built.status,
13334
+ items: null,
13335
+ message: null
13336
+ };
13280
13337
  }
13281
13338
  }).catch((e) => {
13282
- inboxState = { loaded: true, loading: false, status: "error", items: null, message: errMsg(e) };
13339
+ inboxState = {
13340
+ loaded: true,
13341
+ loading: false,
13342
+ status: "error",
13343
+ items: null,
13344
+ message: errMsg(e)
13345
+ };
13283
13346
  }).finally(() => repaint());
13284
13347
  return;
13285
13348
  }
@@ -13313,7 +13376,9 @@ function runHubTui({ input = process.stdin, output = process.stdout, signals, de
13313
13376
  function inboxRows(st) {
13314
13377
  switch (st.status) {
13315
13378
  case "disclosure":
13316
- return ["Messaging disclosure not yet acknowledged \u2014 run `terminalhire inbox` once to review it and enable messages."];
13379
+ return [
13380
+ "Messaging disclosure not yet acknowledged \u2014 run `terminalhire inbox` once to review it and enable messages."
13381
+ ];
13317
13382
  case "not-linked":
13318
13383
  return ["Not linked \u2014 run `terminalhire link` to connect your web session."];
13319
13384
  case "expired":
@@ -13356,11 +13421,13 @@ function runHubTui({ input = process.stdin, output = process.stdout, signals, de
13356
13421
  case "empty":
13357
13422
  return ["No bounties available right now. Check back through the day."];
13358
13423
  case "demoted":
13359
- return ["Paid bounties paused \u2014 run `bounties --priced` to view them. Try Contribute or Jobs."];
13424
+ return [
13425
+ "Paid bounties paused \u2014 run `bounties --priced` to view them. Try Contribute or Jobs."
13426
+ ];
13360
13427
  case "ok":
13361
13428
  return result.bounties.map((job) => {
13362
13429
  const b = job.bounty || {};
13363
- const amt = b.amountUSD != null ? "$" + b.amountUSD.toLocaleString() : "$\u2014";
13430
+ const amt = formatUsd(b.amountUSD);
13364
13431
  const repo = sanitizeLine(b.repoFullName || job.company);
13365
13432
  const prs = b.competingOpenPRs;
13366
13433
  const contend = prs != null && prs > 0 ? ` \xB7 \u26A0 ${prs} in flight` : "";
@@ -13415,7 +13482,9 @@ function runHubTui({ input = process.stdin, output = process.stdout, signals, de
13415
13482
  if (it.note) rows.push(` note: ${sanitizeLine(it.note)}`);
13416
13483
  if (it.contact) rows.push(` contact: ${sanitizeLine(it.contact)}`);
13417
13484
  else if (it.role === "incoming" && it.status === "pending") {
13418
- rows.push(` \u2192 accept: terminalhire intro --accept @${sanitizeLine(it.counterpartyLogin)}`);
13485
+ rows.push(
13486
+ ` \u2192 accept: terminalhire intro --accept @${sanitizeLine(it.counterpartyLogin)}`
13487
+ );
13419
13488
  }
13420
13489
  }
13421
13490
  return rows;
@@ -13502,7 +13571,8 @@ function runHubTui({ input = process.stdin, output = process.stdout, signals, de
13502
13571
  if (name === "Claims") {
13503
13572
  if (!claimsState.loaded) return ["Loading\u2026"];
13504
13573
  if (claimsState.error) return ["Could not read claims \u2014 " + claimsState.error];
13505
- if (!claimsState.rows.length) return ["No claims yet \u2014 run `terminalhire claim <url>` to start one."];
13574
+ if (!claimsState.rows.length)
13575
+ return ["No claims yet \u2014 run `terminalhire claim <url>` to start one."];
13506
13576
  return claimsState.rows.map(formatClaimRow);
13507
13577
  }
13508
13578
  if (name === "Profile") {
@@ -13532,8 +13602,10 @@ function runHubTui({ input = process.stdin, output = process.stdout, signals, de
13532
13602
  const st = listState[name];
13533
13603
  if (st.loaded && !st.error && st.result && st.result.status === "ok") {
13534
13604
  if (name === "Jobs") return `${st.result.ranked.length} roles matching your profile`;
13535
- if (name === "Bounties") return `${st.result.bounties.length} bounties you could knock out`;
13536
- if (name === "Devs") return `${st.result.results.length} matches in the builder directory`;
13605
+ if (name === "Bounties")
13606
+ return `${st.result.bounties.length} bounties you could knock out`;
13607
+ if (name === "Devs")
13608
+ return `${st.result.results.length} matches in the builder directory`;
13537
13609
  }
13538
13610
  return name;
13539
13611
  }
@@ -13563,7 +13635,9 @@ function runHubTui({ input = process.stdin, output = process.stdout, signals, de
13563
13635
  const row = startRow + i;
13564
13636
  if (row < 0 || row >= rows) continue;
13565
13637
  const t = TH_GLYPH.length <= 1 ? 0 : i / (TH_GLYPH.length - 1);
13566
- drawText(buf, cols, row, startCol, TH_GLYPH[i], { fg: gradientFg(BRAND_GRADIENT, t, level) });
13638
+ drawText(buf, cols, row, startCol, TH_GLYPH[i], {
13639
+ fg: gradientFg(BRAND_GRADIENT, t, level)
13640
+ });
13567
13641
  }
13568
13642
  const hint = "press any key to continue";
13569
13643
  const hintRow = Math.min(rows - 1, startRow + TH_GLYPH.length + 1);
@@ -10578,6 +10578,50 @@ var init_cache_store = __esm({
10578
10578
  }
10579
10579
  });
10580
10580
 
10581
+ // bin/sanitize.js
10582
+ function sanitizeText(s) {
10583
+ if (s == null) return "";
10584
+ return String(s).replace(WHITESPACE_CONTROLS, " ").replace(CONTROL_CHARS, "");
10585
+ }
10586
+ function formatUsd(a) {
10587
+ if (typeof a === "number") return Number.isFinite(a) ? "$" + a.toLocaleString() : "$\u2014";
10588
+ if (typeof a !== "string" || a.trim() === "") return "$\u2014";
10589
+ const n = Number(a);
10590
+ return Number.isFinite(n) ? "$" + n.toLocaleString() : "$\u2014";
10591
+ }
10592
+ function safeHttpUrl(url) {
10593
+ if (url == null) return null;
10594
+ const raw = String(url);
10595
+ CONTROL_CHARS.lastIndex = 0;
10596
+ if (CONTROL_CHARS.test(raw)) return null;
10597
+ let parsed;
10598
+ try {
10599
+ parsed = new URL(raw);
10600
+ } catch {
10601
+ return null;
10602
+ }
10603
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
10604
+ return parsed.href;
10605
+ }
10606
+ function linkTitle(title, url) {
10607
+ const safeTitle = sanitizeText(title);
10608
+ const href = safeHttpUrl(url);
10609
+ const isTTY = process.stdout.isTTY;
10610
+ const noColor = process.env["NO_COLOR"] !== void 0;
10611
+ if (isTTY && !noColor && href) {
10612
+ return `\x1B]8;;${href}\x1B\\${safeTitle}\x1B]8;;\x1B\\`;
10613
+ }
10614
+ return href ? `${safeTitle} (${href})` : safeTitle;
10615
+ }
10616
+ var WHITESPACE_CONTROLS, CONTROL_CHARS;
10617
+ var init_sanitize = __esm({
10618
+ "bin/sanitize.js"() {
10619
+ "use strict";
10620
+ WHITESPACE_CONTROLS = /[\t\n\v\f\r]+/g;
10621
+ CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
10622
+ }
10623
+ });
10624
+
10581
10625
  // bin/spinner-io.js
10582
10626
  import {
10583
10627
  readFileSync as readFileSync4,
@@ -11291,7 +11335,7 @@ function buildTipsDetailed(topMatches, baseUrl, max = 8, opts = {}) {
11291
11335
  const token = jobShortToken(String(m.id));
11292
11336
  const url = `${base}/j/${token}`;
11293
11337
  if (source === "bounty") {
11294
- const money = m.amountUSD != null ? `$${Number(m.amountUSD).toLocaleString()}` : "$\u2014";
11338
+ const money = formatUsd(m.amountUSD);
11295
11339
  const repo = m.repo || companyRaw;
11296
11340
  out.push(`\u{1F48E} ${money} \xB7 ${title} \xB7 ${repo} \xB7 ${pct}% \u2014 ${url}`);
11297
11341
  } else if (source === "contribute") {
@@ -11361,6 +11405,7 @@ var init_spinner_render = __esm({
11361
11405
  init_spinner_verbs();
11362
11406
  init_spinner_seen();
11363
11407
  init_spinner_io();
11408
+ init_sanitize();
11364
11409
  init_src();
11365
11410
  }
11366
11411
  });
@@ -12297,39 +12342,7 @@ function markClicked(id) {
12297
12342
 
12298
12343
  // bin/jpi-jobs.js
12299
12344
  init_cache_store();
12300
-
12301
- // bin/sanitize.js
12302
- var CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
12303
- function sanitizeText(s) {
12304
- if (s == null) return "";
12305
- return String(s).replace(CONTROL_CHARS, "");
12306
- }
12307
- function safeHttpUrl(url) {
12308
- if (url == null) return null;
12309
- const raw = String(url);
12310
- CONTROL_CHARS.lastIndex = 0;
12311
- if (CONTROL_CHARS.test(raw)) return null;
12312
- let parsed;
12313
- try {
12314
- parsed = new URL(raw);
12315
- } catch {
12316
- return null;
12317
- }
12318
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
12319
- return parsed.href;
12320
- }
12321
- function linkTitle(title, url) {
12322
- const safeTitle = sanitizeText(title);
12323
- const href = safeHttpUrl(url);
12324
- const isTTY = process.stdout.isTTY;
12325
- const noColor = process.env["NO_COLOR"] !== void 0;
12326
- if (isTTY && !noColor && href) {
12327
- return `\x1B]8;;${href}\x1B\\${safeTitle}\x1B]8;;\x1B\\`;
12328
- }
12329
- return href ? `${safeTitle} (${href})` : safeTitle;
12330
- }
12331
-
12332
- // bin/jpi-jobs.js
12345
+ init_sanitize();
12333
12346
  var __dirname2 = fileURLToPath3(new URL(".", import.meta.url));
12334
12347
  var TERMINALHIRE_DIR6 = process.env.TERMINALHIRE_DIR || join14(homedir9(), ".terminalhire");
12335
12348
  var INDEX_CACHE_FILE2 = join14(TERMINALHIRE_DIR6, "index-cache.json");
@@ -10980,12 +10980,19 @@ var init_open_url = __esm({
10980
10980
  // bin/sanitize.js
10981
10981
  function sanitizeText(s) {
10982
10982
  if (s == null) return "";
10983
- return String(s).replace(CONTROL_CHARS, "");
10983
+ return String(s).replace(WHITESPACE_CONTROLS, " ").replace(CONTROL_CHARS, "");
10984
10984
  }
10985
- var CONTROL_CHARS;
10985
+ function formatUsd(a) {
10986
+ if (typeof a === "number") return Number.isFinite(a) ? "$" + a.toLocaleString() : "$\u2014";
10987
+ if (typeof a !== "string" || a.trim() === "") return "$\u2014";
10988
+ const n = Number(a);
10989
+ return Number.isFinite(n) ? "$" + n.toLocaleString() : "$\u2014";
10990
+ }
10991
+ var WHITESPACE_CONTROLS, CONTROL_CHARS;
10986
10992
  var init_sanitize = __esm({
10987
10993
  "bin/sanitize.js"() {
10988
10994
  "use strict";
10995
+ WHITESPACE_CONTROLS = /[\t\n\v\f\r]+/g;
10989
10996
  CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
10990
10997
  }
10991
10998
  });
@@ -11141,7 +11148,7 @@ function nowISO() {
11141
11148
  return (/* @__PURE__ */ new Date()).toISOString();
11142
11149
  }
11143
11150
  function defangText(s) {
11144
- return typeof s === "string" ? s.replace(CONTROL_CHARS2, "") : s;
11151
+ return typeof s === "string" ? s.replace(WHITESPACE_CONTROLS2, " ").replace(CONTROL_CHARS2, "") : s;
11145
11152
  }
11146
11153
  function finiteAmount(a) {
11147
11154
  if (typeof a === "number") return Number.isFinite(a) ? a : null;
@@ -11279,7 +11286,7 @@ function acceptedPRRate(claims = readClaims()) {
11279
11286
  const merged = claims.filter((c) => c.state === "merged").length;
11280
11287
  return { merged, total, rate: total === 0 ? 0 : merged / total };
11281
11288
  }
11282
- var TERMINALHIRE_DIR5, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, CONTROL_CHARS2;
11289
+ var TERMINALHIRE_DIR5, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, WHITESPACE_CONTROLS2, CONTROL_CHARS2;
11283
11290
  var init_claims = __esm({
11284
11291
  "src/claims.ts"() {
11285
11292
  "use strict";
@@ -11335,6 +11342,7 @@ var init_claims = __esm({
11335
11342
  ]),
11336
11343
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
11337
11344
  };
11345
+ WHITESPACE_CONTROLS2 = /[\t\n\v\f\r]+/g;
11338
11346
  CONTROL_CHARS2 = /[\x00-\x1f\x7f-\x9f]/g;
11339
11347
  }
11340
11348
  });
@@ -27157,10 +27165,7 @@ async function pollPR(prUrl) {
27157
27165
  }
27158
27166
  }
27159
27167
  function fmtAmount(a) {
27160
- if (a == null) return "$\u2014";
27161
- if (typeof a === "string" && a.trim() === "") return "$\u2014";
27162
- const n = typeof a === "number" ? a : Number(a);
27163
- return Number.isFinite(n) ? "$" + n.toLocaleString() : "$\u2014";
27168
+ return formatUsd(a);
27164
27169
  }
27165
27170
  function fmtClaimAmount(c) {
27166
27171
  return c.kind === "contribution" ? "contribution" : fmtAmount(c.amountUSD);
@@ -27299,8 +27304,15 @@ async function resolveBounty(arg) {
27299
27304
  return {
27300
27305
  bountyId,
27301
27306
  indexNativeId,
27302
- title,
27303
- repoFullName,
27307
+ // DEFANGED HERE, at the one place every tier of this resolver converges.
27308
+ // `claims.ts` cleans what comes OUT of the local store, which covers every later
27309
+ // read — but this object is printed (the preview card, the record card) and
27310
+ // handed to `recordClaim` BEFORE any store exists for it, so on a claim's first
27311
+ // use the store boundary has not run yet. `repoFullName` also goes on to be
27312
+ // interpolated into GitHub API URLs from here. Found by a cross-model review of
27313
+ // TERM-380; the store fix alone left this path open.
27314
+ title: sanitizeText(title),
27315
+ repoFullName: repoFullName == null ? repoFullName : sanitizeText(repoFullName),
27304
27316
  issueUrl,
27305
27317
  amountUSD,
27306
27318
  source,
@@ -28521,7 +28533,7 @@ async function cmdStart(id, flags = {}) {
28521
28533
  console.error(`terminalhire claim: no claim with id '${id}'.`);
28522
28534
  process.exit(1);
28523
28535
  }
28524
- if (claim.approval?.state === "pending" && !approvalsChecked) {
28536
+ if (claim.approval?.state === "pending") {
28525
28537
  ({ approvalsChecked, approvalsUnavailable } = await syncFounderApprovals(claims, [claim]));
28526
28538
  claim = claims.findClaim(id);
28527
28539
  }
@@ -12236,6 +12236,19 @@ var init_spinner_verbs = __esm({
12236
12236
  }
12237
12237
  });
12238
12238
 
12239
+ // bin/sanitize.js
12240
+ function formatUsd(a) {
12241
+ if (typeof a === "number") return Number.isFinite(a) ? "$" + a.toLocaleString() : "$\u2014";
12242
+ if (typeof a !== "string" || a.trim() === "") return "$\u2014";
12243
+ const n = Number(a);
12244
+ return Number.isFinite(n) ? "$" + n.toLocaleString() : "$\u2014";
12245
+ }
12246
+ var init_sanitize = __esm({
12247
+ "bin/sanitize.js"() {
12248
+ "use strict";
12249
+ }
12250
+ });
12251
+
12239
12252
  // bin/spinner-render.js
12240
12253
  function interleaveBySource(topMatches) {
12241
12254
  if (!Array.isArray(topMatches) || topMatches.length === 0) return topMatches;
@@ -12317,7 +12330,7 @@ function buildTipsDetailed(topMatches, baseUrl, max = 8, opts = {}) {
12317
12330
  const token = jobShortToken(String(m.id));
12318
12331
  const url = `${base}/j/${token}`;
12319
12332
  if (source === "bounty") {
12320
- const money = m.amountUSD != null ? `$${Number(m.amountUSD).toLocaleString()}` : "$\u2014";
12333
+ const money = formatUsd(m.amountUSD);
12321
12334
  const repo = m.repo || companyRaw;
12322
12335
  out.push(`\u{1F48E} ${money} \xB7 ${title} \xB7 ${repo} \xB7 ${pct}% \u2014 ${url}`);
12323
12336
  } else if (source === "contribute") {
@@ -12387,6 +12400,7 @@ var init_spinner_render = __esm({
12387
12400
  init_spinner_verbs();
12388
12401
  init_spinner_seen();
12389
12402
  init_spinner_io();
12403
+ init_sanitize();
12390
12404
  init_src();
12391
12405
  }
12392
12406
  });
@@ -12563,7 +12577,7 @@ function nowISO() {
12563
12577
  return (/* @__PURE__ */ new Date()).toISOString();
12564
12578
  }
12565
12579
  function defangText(s) {
12566
- return typeof s === "string" ? s.replace(CONTROL_CHARS, "") : s;
12580
+ return typeof s === "string" ? s.replace(WHITESPACE_CONTROLS, " ").replace(CONTROL_CHARS, "") : s;
12567
12581
  }
12568
12582
  function finiteAmount(a) {
12569
12583
  if (typeof a === "number") return Number.isFinite(a) ? a : null;
@@ -12701,7 +12715,7 @@ function acceptedPRRate(claims = readClaims()) {
12701
12715
  const merged = claims.filter((c) => c.state === "merged").length;
12702
12716
  return { merged, total, rate: total === 0 ? 0 : merged / total };
12703
12717
  }
12704
- var TERMINALHIRE_DIR8, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS2, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, CONTROL_CHARS;
12718
+ var TERMINALHIRE_DIR8, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS2, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, WHITESPACE_CONTROLS, CONTROL_CHARS;
12705
12719
  var init_claims = __esm({
12706
12720
  "src/claims.ts"() {
12707
12721
  "use strict";
@@ -12757,6 +12771,7 @@ var init_claims = __esm({
12757
12771
  ]),
12758
12772
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
12759
12773
  };
12774
+ WHITESPACE_CONTROLS = /[\t\n\v\f\r]+/g;
12760
12775
  CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
12761
12776
  }
12762
12777
  });
@@ -1910,7 +1910,7 @@ function nowISO2() {
1910
1910
  return (/* @__PURE__ */ new Date()).toISOString();
1911
1911
  }
1912
1912
  function defangText(s) {
1913
- return typeof s === "string" ? s.replace(CONTROL_CHARS, "") : s;
1913
+ return typeof s === "string" ? s.replace(WHITESPACE_CONTROLS, " ").replace(CONTROL_CHARS, "") : s;
1914
1914
  }
1915
1915
  function finiteAmount(a) {
1916
1916
  if (typeof a === "number") return Number.isFinite(a) ? a : null;
@@ -2048,7 +2048,7 @@ function acceptedPRRate(claims = readClaims()) {
2048
2048
  const merged = claims.filter((c) => c.state === "merged").length;
2049
2049
  return { merged, total, rate: total === 0 ? 0 : merged / total };
2050
2050
  }
2051
- var TERMINALHIRE_DIR4, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, CONTROL_CHARS;
2051
+ var TERMINALHIRE_DIR4, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, CLAIM_STATES, PUSHED_CLAIM_FIELDS, TERMINAL_STATES, POLL_TRANSITIONS, WHITESPACE_CONTROLS, CONTROL_CHARS;
2052
2052
  var init_claims = __esm({
2053
2053
  "src/claims.ts"() {
2054
2054
  "use strict";
@@ -2104,6 +2104,7 @@ var init_claims = __esm({
2104
2104
  ]),
2105
2105
  submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
2106
2106
  };
2107
+ WHITESPACE_CONTROLS = /[\t\n\v\f\r]+/g;
2107
2108
  CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
2108
2109
  }
2109
2110
  });
@@ -1,8 +1,15 @@
1
1
  // bin/sanitize.js
2
+ var WHITESPACE_CONTROLS = /[\t\n\v\f\r]+/g;
2
3
  var CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
3
4
  function sanitizeText(s) {
4
5
  if (s == null) return "";
5
- return String(s).replace(CONTROL_CHARS, "");
6
+ return String(s).replace(WHITESPACE_CONTROLS, " ").replace(CONTROL_CHARS, "");
7
+ }
8
+ function formatUsd(a) {
9
+ if (typeof a === "number") return Number.isFinite(a) ? "$" + a.toLocaleString() : "$\u2014";
10
+ if (typeof a !== "string" || a.trim() === "") return "$\u2014";
11
+ const n = Number(a);
12
+ return Number.isFinite(n) ? "$" + n.toLocaleString() : "$\u2014";
6
13
  }
7
14
  function safeHttpUrl(url) {
8
15
  if (url == null) return null;
@@ -29,6 +36,7 @@ function linkTitle(title, url) {
29
36
  return href ? `${safeTitle} (${href})` : safeTitle;
30
37
  }
31
38
  export {
39
+ formatUsd,
32
40
  linkTitle,
33
41
  safeHttpUrl,
34
42
  sanitizeText
@@ -607,6 +607,14 @@ function clearSpinnerTips() {
607
607
  return { cleared: true };
608
608
  }
609
609
 
610
+ // bin/sanitize.js
611
+ function formatUsd(a) {
612
+ if (typeof a === "number") return Number.isFinite(a) ? "$" + a.toLocaleString() : "$\u2014";
613
+ if (typeof a !== "string" || a.trim() === "") return "$\u2014";
614
+ const n = Number(a);
615
+ return Number.isFinite(n) ? "$" + n.toLocaleString() : "$\u2014";
616
+ }
617
+
610
618
  // ../../packages/core/src/vocab/graph.data.ts
611
619
  var VOCAB_NODES = [
612
620
  // ── Languages ─────────────────────────────────────────────────────────────
@@ -1404,7 +1412,7 @@ function buildTipsDetailed(topMatches, baseUrl, max = 8, opts = {}) {
1404
1412
  const token = jobShortToken(String(m.id));
1405
1413
  const url = `${base}/j/${token}`;
1406
1414
  if (source === "bounty") {
1407
- const money = m.amountUSD != null ? `$${Number(m.amountUSD).toLocaleString()}` : "$\u2014";
1415
+ const money = formatUsd(m.amountUSD);
1408
1416
  const repo = m.repo || companyRaw;
1409
1417
  out.push(`\u{1F48E} ${money} \xB7 ${title} \xB7 ${repo} \xB7 ${pct}% \u2014 ${url}`);
1410
1418
  } else if (source === "contribute") {
@@ -635,6 +635,14 @@ function buildSpinnerPool(topMatches, max = 6, opts = {}) {
635
635
  return withStale(pool);
636
636
  }
637
637
 
638
+ // bin/sanitize.js
639
+ function formatUsd(a) {
640
+ if (typeof a === "number") return Number.isFinite(a) ? "$" + a.toLocaleString() : "$\u2014";
641
+ if (typeof a !== "string" || a.trim() === "") return "$\u2014";
642
+ const n = Number(a);
643
+ return Number.isFinite(n) ? "$" + n.toLocaleString() : "$\u2014";
644
+ }
645
+
638
646
  // ../../packages/core/src/vocab/graph.data.ts
639
647
  var VOCAB_NODES = [
640
648
  // ── Languages ─────────────────────────────────────────────────────────────
@@ -1432,7 +1440,7 @@ function buildTipsDetailed(topMatches, baseUrl, max = 8, opts = {}) {
1432
1440
  const token = jobShortToken(String(m.id));
1433
1441
  const url = `${base}/j/${token}`;
1434
1442
  if (source === "bounty") {
1435
- const money = m.amountUSD != null ? `$${Number(m.amountUSD).toLocaleString()}` : "$\u2014";
1443
+ const money = formatUsd(m.amountUSD);
1436
1444
  const repo = m.repo || companyRaw;
1437
1445
  out.push(`\u{1F48E} ${money} \xB7 ${title} \xB7 ${repo} \xB7 ${pct}% \u2014 ${url}`);
1438
1446
  } else if (source === "contribute") {
@@ -161,9 +161,10 @@ function nextPolledState(from, observed) {
161
161
  function nowISO() {
162
162
  return (/* @__PURE__ */ new Date()).toISOString();
163
163
  }
164
+ var WHITESPACE_CONTROLS = /[\t\n\v\f\r]+/g;
164
165
  var CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
165
166
  function defangText(s) {
166
- return typeof s === "string" ? s.replace(CONTROL_CHARS, "") : s;
167
+ return typeof s === "string" ? s.replace(WHITESPACE_CONTROLS, " ").replace(CONTROL_CHARS, "") : s;
167
168
  }
168
169
  function finiteAmount(a) {
169
170
  if (typeof a === "number") return Number.isFinite(a) ? a : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminalhire",
3
- "version": "0.42.1",
3
+ "version": "0.42.2",
4
4
  "description": "Local-first job matching for developers — ambient job matches in the Claude Code spinner. Your profile never leaves your machine.",
5
5
  "repository": {
6
6
  "type": "git",