terminalhire 0.32.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2026,7 +2026,7 @@ function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
2026
2026
  reason: buildReason(details)
2027
2027
  };
2028
2028
  });
2029
- return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => {
2029
+ return scored.filter((r) => r !== null && r.score >= (opts.minScore ?? MIN_SCORE)).sort((a, b) => {
2030
2030
  const byScore = b.score - a.score;
2031
2031
  if (Math.abs(byScore) > TIEBREAK_EPS) return byScore;
2032
2032
  const byAcceptance = (b.acceptance?.count ?? 0) - (a.acceptance?.count ?? 0);
@@ -8353,11 +8353,134 @@ var init_profile = __esm({
8353
8353
  }
8354
8354
  });
8355
8355
 
8356
- // bin/jpi-bounties.js
8357
- init_src();
8358
- import { readFileSync as readFileSync4 } from "fs";
8356
+ // src/claims.ts
8357
+ var claims_exports = {};
8358
+ __export(claims_exports, {
8359
+ PUSHED_CLAIM_FIELDS: () => PUSHED_CLAIM_FIELDS,
8360
+ acceptedPRRate: () => acceptedPRRate,
8361
+ findClaim: () => findClaim,
8362
+ listClaims: () => listClaims,
8363
+ readClaims: () => readClaims,
8364
+ recordClaim: () => recordClaim,
8365
+ removeClaim: () => removeClaim,
8366
+ toPushedClaim: () => toPushedClaim,
8367
+ updateClaim: () => updateClaim
8368
+ });
8369
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, renameSync as renameSync2, existsSync as existsSync2 } from "fs";
8359
8370
  import { join as join4 } from "path";
8360
8371
  import { homedir as homedir3 } from "os";
8372
+ function toPushedClaim(claim) {
8373
+ return {
8374
+ kind: claim.kind,
8375
+ repoFullName: claim.repoFullName,
8376
+ state: claim.state,
8377
+ prUrl: claim.prUrl,
8378
+ merged: claim.state === "merged",
8379
+ claimedAt: claim.claimedAt,
8380
+ updatedAt: claim.updatedAt
8381
+ };
8382
+ }
8383
+ function nowISO() {
8384
+ return (/* @__PURE__ */ new Date()).toISOString();
8385
+ }
8386
+ function normalizeClaim(c) {
8387
+ return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
8388
+ }
8389
+ function readClaims() {
8390
+ try {
8391
+ if (!existsSync2(CLAIMS_FILE)) return [];
8392
+ const data = JSON.parse(readFileSync4(CLAIMS_FILE, "utf8"));
8393
+ const claims = Array.isArray(data?.claims) ? data.claims : [];
8394
+ return claims.map(normalizeClaim);
8395
+ } catch {
8396
+ return [];
8397
+ }
8398
+ }
8399
+ function writeClaims(claims) {
8400
+ mkdirSync3(TERMINALHIRE_DIR3, { recursive: true });
8401
+ const tmp = `${CLAIMS_FILE}.tmp`;
8402
+ const payload = { claims };
8403
+ writeFileSync3(tmp, JSON.stringify(payload, null, 2), "utf8");
8404
+ renameSync2(tmp, CLAIMS_FILE);
8405
+ }
8406
+ function findClaim(id) {
8407
+ return readClaims().find((c) => c.id === id) ?? null;
8408
+ }
8409
+ function listClaims(opts = {}) {
8410
+ const claims = readClaims();
8411
+ if (!opts.active) return claims;
8412
+ return claims.filter((c) => !TERMINAL_STATES.has(c.state));
8413
+ }
8414
+ function recordClaim(rec) {
8415
+ const claims = readClaims();
8416
+ if (claims.some((c) => c.id === rec.id)) {
8417
+ throw new Error(
8418
+ `claim already exists for '${rec.id}' \u2014 run 'terminalhire claim status ${rec.id}' or 'terminalhire claim release ${rec.id}'`
8419
+ );
8420
+ }
8421
+ const ts = nowISO();
8422
+ const claim = {
8423
+ ...rec,
8424
+ // Defensive default (mirrors normalizeClaim's `kind ?? 'bounty'` pattern):
8425
+ // a caller written before `policy` existed, or a plain-JS caller that skips
8426
+ // it, still produces a valid record instead of `policy: undefined`.
8427
+ policy: rec.policy ?? null,
8428
+ state: "claimed",
8429
+ worktreePath: null,
8430
+ branch: null,
8431
+ prUrl: null,
8432
+ review: null,
8433
+ claimedAt: ts,
8434
+ updatedAt: ts
8435
+ };
8436
+ claims.push(claim);
8437
+ writeClaims(claims);
8438
+ return claim;
8439
+ }
8440
+ function updateClaim(id, patch) {
8441
+ const claims = readClaims();
8442
+ const idx = claims.findIndex((c) => c.id === id);
8443
+ if (idx === -1) return null;
8444
+ claims[idx] = { ...claims[idx], ...patch, updatedAt: nowISO() };
8445
+ writeClaims(claims);
8446
+ return claims[idx];
8447
+ }
8448
+ function removeClaim(id) {
8449
+ const claims = readClaims();
8450
+ const next = claims.filter((c) => c.id !== id);
8451
+ if (next.length === claims.length) return false;
8452
+ writeClaims(next);
8453
+ return true;
8454
+ }
8455
+ function acceptedPRRate(claims = readClaims()) {
8456
+ const total = claims.length;
8457
+ const merged = claims.filter((c) => c.state === "merged").length;
8458
+ return { merged, total, rate: total === 0 ? 0 : merged / total };
8459
+ }
8460
+ var TERMINALHIRE_DIR3, CLAIMS_FILE, PUSHED_CLAIM_FIELDS, TERMINAL_STATES;
8461
+ var init_claims = __esm({
8462
+ "src/claims.ts"() {
8463
+ "use strict";
8464
+ TERMINALHIRE_DIR3 = process.env.TERMINALHIRE_DIR || join4(homedir3(), ".terminalhire");
8465
+ CLAIMS_FILE = join4(TERMINALHIRE_DIR3, "claims.json");
8466
+ PUSHED_CLAIM_FIELDS = [
8467
+ "kind",
8468
+ "repoFullName",
8469
+ "state",
8470
+ "prUrl",
8471
+ "merged",
8472
+ "claimedAt",
8473
+ "updatedAt"
8474
+ ];
8475
+ TERMINAL_STATES = /* @__PURE__ */ new Set(["merged", "abandoned"]);
8476
+ }
8477
+ });
8478
+
8479
+ // bin/jpi-bounties.js
8480
+ init_src();
8481
+ import { readFileSync as readFileSync5 } from "fs";
8482
+ import { join as join5 } from "path";
8483
+ import { homedir as homedir4 } from "os";
8361
8484
  import { createInterface } from "readline";
8362
8485
 
8363
8486
  // bin/cache-store.js
@@ -8422,8 +8545,8 @@ function linkTitle(title, url) {
8422
8545
  }
8423
8546
 
8424
8547
  // bin/jpi-bounties.js
8425
- var TERMINALHIRE_DIR3 = process.env.TERMINALHIRE_DIR || join4(homedir3(), ".terminalhire");
8426
- var INDEX_CACHE_FILE2 = join4(TERMINALHIRE_DIR3, "index-cache.json");
8548
+ var TERMINALHIRE_DIR4 = process.env.TERMINALHIRE_DIR || join5(homedir4(), ".terminalhire");
8549
+ var INDEX_CACHE_FILE2 = join5(TERMINALHIRE_DIR4, "index-cache.json");
8427
8550
  var INDEX_TTL_MS = 15 * 60 * 1e3;
8428
8551
  var API_URL = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
8429
8552
  var RANK_MODE = process.env["TERMINALHIRE_BOUNTY_RANK"] ?? "winnability";
@@ -8436,7 +8559,7 @@ var SHOW_ALL = args.includes("--all");
8436
8559
  var WINNABLE_ONLY = args.includes("--winnable");
8437
8560
  function readIndexCache() {
8438
8561
  try {
8439
- const entry = JSON.parse(readFileSync4(INDEX_CACHE_FILE2, "utf8"));
8562
+ const entry = JSON.parse(readFileSync5(INDEX_CACHE_FILE2, "utf8"));
8440
8563
  if (Date.now() - entry.ts < INDEX_TTL_MS) return entry.index;
8441
8564
  return null;
8442
8565
  } catch {
@@ -8468,22 +8591,26 @@ function formatAmount(b) {
8468
8591
  return b.amountUSD != null ? "$" + b.amountUSD.toLocaleString() : "$\u2014";
8469
8592
  }
8470
8593
  var EFFORT_LABEL = { small: "small (~\xBD day)", medium: "medium (~1 day)", large: "large (multi-day)" };
8471
- function printBounty(i, job, score, reason, matchedTags) {
8594
+ function printBounty(i, job, score, reason, matchedTags, claimedIds = /* @__PURE__ */ new Set()) {
8472
8595
  const b = job.bounty ?? {};
8473
8596
  const stars = b.repoStars != null ? ` \xB7 ${b.repoStars}\u2605` : "";
8474
8597
  const effort = b.estimatedEffort ? ` \xB7 ${EFFORT_LABEL[b.estimatedEffort]}` : "";
8475
8598
  const scoreStr = score > 0 ? ` \xB7 match ${Math.round(score * 100)}%` : "";
8476
8599
  const prs = b.competingOpenPRs;
8477
8600
  const contend = prs != null && prs > 0 ? ` \xB7 \u26A0 ${prs} PR${prs === 1 ? "" : "s"} in flight` : "";
8601
+ const claimed = claimedIds.has(job.id);
8602
+ const badge = claimed ? " \xB7 \u25CF claimed by you" : "";
8478
8603
  const ref = opportunityShortToken(job.id);
8479
8604
  console.log(`
8480
8605
  ${i + 1}. ${linkTitle(job.title, job.url)} [${ref}]`);
8481
- console.log(` ${formatAmount(b)}${effort} \xB7 ${sanitizeText(b.repoFullName ?? job.company)}${stars}${scoreStr}${contend}`);
8606
+ console.log(` ${formatAmount(b)}${effort} \xB7 ${sanitizeText(b.repoFullName ?? job.company)}${stars}${scoreStr}${contend}${badge}`);
8482
8607
  if (reason) console.log(` ${reason}`);
8483
8608
  if (matchedTags && matchedTags.length) console.log(` Tags matched: ${matchedTags.slice(0, 5).join(", ")}`);
8484
8609
  console.log(` id: ${job.id}`);
8485
8610
  console.log(` Claim: ${sanitizeText(b.claimUrl ?? job.url)}`);
8486
- console.log(` \u2192 terminalhire claim ${ref}`);
8611
+ console.log(
8612
+ claimed ? ` \u2192 claimed by you \u2014 terminalhire claim status ${job.id}` : ` \u2192 terminalhire claim ${ref}`
8613
+ );
8487
8614
  }
8488
8615
  function rankBounties(bounties, { rankMode = "winnability", scoreOf = () => 0 } = {}) {
8489
8616
  const legacy = rankMode === "legacy";
@@ -8561,6 +8688,12 @@ async function run() {
8561
8688
  if (result.status !== "ok") return;
8562
8689
  const { bounties, ranked, matchedCount } = result;
8563
8690
  const shown = SHOW_ALL ? bounties : bounties.slice(0, LIMIT);
8691
+ let claimedIds = /* @__PURE__ */ new Set();
8692
+ try {
8693
+ const { listClaims: listClaims2 } = await Promise.resolve().then(() => (init_claims(), claims_exports));
8694
+ claimedIds = new Set(listClaims2({ active: true }).map((c) => c.id));
8695
+ } catch {
8696
+ }
8564
8697
  console.log(
8565
8698
  `
8566
8699
  \u26A1 ${bounties.length} bount${bounties.length === 1 ? "y" : "ies"} you could knock out` + (matchedCount ? ` \u2014 ${matchedCount} matched to your profile` : "") + ` (local rank \u2014 no data sent)
@@ -8568,7 +8701,7 @@ async function run() {
8568
8701
  );
8569
8702
  for (let i = 0; i < shown.length; i++) {
8570
8703
  const r = ranked.get(shown[i].id);
8571
- printBounty(i, shown[i], r?.score ?? 0, r?.reason, r?.matchedTags);
8704
+ printBounty(i, shown[i], r?.score ?? 0, r?.reason, r?.matchedTags, claimedIds);
8572
8705
  }
8573
8706
  if (!SHOW_ALL && bounties.length > shown.length) {
8574
8707
  console.log(`
@@ -8595,6 +8728,7 @@ export {
8595
8728
  classifyEmptyStatus,
8596
8729
  filterPaidVisibility,
8597
8730
  getBounties,
8731
+ printBounty,
8598
8732
  rankBounties,
8599
8733
  run
8600
8734
  };
@@ -2026,7 +2026,7 @@ function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
2026
2026
  reason: buildReason(details)
2027
2027
  };
2028
2028
  });
2029
- return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => {
2029
+ return scored.filter((r) => r !== null && r.score >= (opts.minScore ?? MIN_SCORE)).sort((a, b) => {
2030
2030
  const byScore = b.score - a.score;
2031
2031
  if (Math.abs(byScore) > TIEBREAK_EPS) return byScore;
2032
2032
  const byAcceptance = (b.acceptance?.count ?? 0) - (a.acceptance?.count ?? 0);
@@ -868,7 +868,7 @@ function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
868
868
  reason: buildReason(details)
869
869
  };
870
870
  });
871
- return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => {
871
+ return scored.filter((r) => r !== null && r.score >= (opts.minScore ?? MIN_SCORE)).sort((a, b) => {
872
872
  const byScore = b.score - a.score;
873
873
  if (Math.abs(byScore) > TIEBREAK_EPS) return byScore;
874
874
  const byAcceptance = (b.acceptance?.count ?? 0) - (a.acceptance?.count ?? 0);
@@ -1704,11 +1704,134 @@ var init_profile = __esm({
1704
1704
  }
1705
1705
  });
1706
1706
 
1707
- // bin/jpi-contribute.js
1708
- init_src();
1709
- import { readFileSync as readFileSync5 } from "fs";
1707
+ // src/claims.ts
1708
+ var claims_exports = {};
1709
+ __export(claims_exports, {
1710
+ PUSHED_CLAIM_FIELDS: () => PUSHED_CLAIM_FIELDS,
1711
+ acceptedPRRate: () => acceptedPRRate,
1712
+ findClaim: () => findClaim,
1713
+ listClaims: () => listClaims,
1714
+ readClaims: () => readClaims,
1715
+ recordClaim: () => recordClaim,
1716
+ removeClaim: () => removeClaim,
1717
+ toPushedClaim: () => toPushedClaim,
1718
+ updateClaim: () => updateClaim
1719
+ });
1720
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, renameSync as renameSync2, existsSync as existsSync3 } from "fs";
1710
1721
  import { join as join5 } from "path";
1711
1722
  import { homedir as homedir4 } from "os";
1723
+ function toPushedClaim(claim) {
1724
+ return {
1725
+ kind: claim.kind,
1726
+ repoFullName: claim.repoFullName,
1727
+ state: claim.state,
1728
+ prUrl: claim.prUrl,
1729
+ merged: claim.state === "merged",
1730
+ claimedAt: claim.claimedAt,
1731
+ updatedAt: claim.updatedAt
1732
+ };
1733
+ }
1734
+ function nowISO() {
1735
+ return (/* @__PURE__ */ new Date()).toISOString();
1736
+ }
1737
+ function normalizeClaim(c) {
1738
+ return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
1739
+ }
1740
+ function readClaims() {
1741
+ try {
1742
+ if (!existsSync3(CLAIMS_FILE)) return [];
1743
+ const data = JSON.parse(readFileSync5(CLAIMS_FILE, "utf8"));
1744
+ const claims = Array.isArray(data?.claims) ? data.claims : [];
1745
+ return claims.map(normalizeClaim);
1746
+ } catch {
1747
+ return [];
1748
+ }
1749
+ }
1750
+ function writeClaims(claims) {
1751
+ mkdirSync4(TERMINALHIRE_DIR4, { recursive: true });
1752
+ const tmp = `${CLAIMS_FILE}.tmp`;
1753
+ const payload = { claims };
1754
+ writeFileSync4(tmp, JSON.stringify(payload, null, 2), "utf8");
1755
+ renameSync2(tmp, CLAIMS_FILE);
1756
+ }
1757
+ function findClaim(id) {
1758
+ return readClaims().find((c) => c.id === id) ?? null;
1759
+ }
1760
+ function listClaims(opts = {}) {
1761
+ const claims = readClaims();
1762
+ if (!opts.active) return claims;
1763
+ return claims.filter((c) => !TERMINAL_STATES.has(c.state));
1764
+ }
1765
+ function recordClaim(rec) {
1766
+ const claims = readClaims();
1767
+ if (claims.some((c) => c.id === rec.id)) {
1768
+ throw new Error(
1769
+ `claim already exists for '${rec.id}' \u2014 run 'terminalhire claim status ${rec.id}' or 'terminalhire claim release ${rec.id}'`
1770
+ );
1771
+ }
1772
+ const ts = nowISO();
1773
+ const claim = {
1774
+ ...rec,
1775
+ // Defensive default (mirrors normalizeClaim's `kind ?? 'bounty'` pattern):
1776
+ // a caller written before `policy` existed, or a plain-JS caller that skips
1777
+ // it, still produces a valid record instead of `policy: undefined`.
1778
+ policy: rec.policy ?? null,
1779
+ state: "claimed",
1780
+ worktreePath: null,
1781
+ branch: null,
1782
+ prUrl: null,
1783
+ review: null,
1784
+ claimedAt: ts,
1785
+ updatedAt: ts
1786
+ };
1787
+ claims.push(claim);
1788
+ writeClaims(claims);
1789
+ return claim;
1790
+ }
1791
+ function updateClaim(id, patch) {
1792
+ const claims = readClaims();
1793
+ const idx = claims.findIndex((c) => c.id === id);
1794
+ if (idx === -1) return null;
1795
+ claims[idx] = { ...claims[idx], ...patch, updatedAt: nowISO() };
1796
+ writeClaims(claims);
1797
+ return claims[idx];
1798
+ }
1799
+ function removeClaim(id) {
1800
+ const claims = readClaims();
1801
+ const next = claims.filter((c) => c.id !== id);
1802
+ if (next.length === claims.length) return false;
1803
+ writeClaims(next);
1804
+ return true;
1805
+ }
1806
+ function acceptedPRRate(claims = readClaims()) {
1807
+ const total = claims.length;
1808
+ const merged = claims.filter((c) => c.state === "merged").length;
1809
+ return { merged, total, rate: total === 0 ? 0 : merged / total };
1810
+ }
1811
+ var TERMINALHIRE_DIR4, CLAIMS_FILE, PUSHED_CLAIM_FIELDS, TERMINAL_STATES;
1812
+ var init_claims = __esm({
1813
+ "src/claims.ts"() {
1814
+ "use strict";
1815
+ TERMINALHIRE_DIR4 = process.env.TERMINALHIRE_DIR || join5(homedir4(), ".terminalhire");
1816
+ CLAIMS_FILE = join5(TERMINALHIRE_DIR4, "claims.json");
1817
+ PUSHED_CLAIM_FIELDS = [
1818
+ "kind",
1819
+ "repoFullName",
1820
+ "state",
1821
+ "prUrl",
1822
+ "merged",
1823
+ "claimedAt",
1824
+ "updatedAt"
1825
+ ];
1826
+ TERMINAL_STATES = /* @__PURE__ */ new Set(["merged", "abandoned"]);
1827
+ }
1828
+ });
1829
+
1830
+ // bin/jpi-contribute.js
1831
+ init_src();
1832
+ import { readFileSync as readFileSync6 } from "fs";
1833
+ import { join as join6 } from "path";
1834
+ import { homedir as homedir5 } from "os";
1712
1835
  import { createInterface } from "readline";
1713
1836
 
1714
1837
  // bin/cache-store.js
@@ -1815,8 +1938,8 @@ function linkTitle(title, url) {
1815
1938
  }
1816
1939
 
1817
1940
  // bin/jpi-contribute.js
1818
- var TERMINALHIRE_DIR4 = process.env.TERMINALHIRE_DIR || join5(homedir4(), ".terminalhire");
1819
- var INDEX_CACHE_FILE2 = join5(TERMINALHIRE_DIR4, "index-cache.json");
1941
+ var TERMINALHIRE_DIR5 = process.env.TERMINALHIRE_DIR || join6(homedir5(), ".terminalhire");
1942
+ var INDEX_CACHE_FILE2 = join6(TERMINALHIRE_DIR5, "index-cache.json");
1820
1943
  var INDEX_TTL_MS = 15 * 60 * 1e3;
1821
1944
  var API_URL = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
1822
1945
  var HEADER = "Contribution opportunities \u2014 open issues where a merged PR actually counts toward your r\xE9sum\xE9.\nRepos \u226550\u2605 / \u226510 contributors \xB7 unassigned \xB7 merit-merge \xB7 matched locally to your stack.";
@@ -1824,7 +1947,7 @@ var OPT_IN_PROMPT = "Contribute is off. Turn it on to see open issues where a me
1824
1947
  var EMPTY_STATE = "Nothing clears the bar right now. We only list issues where a merged PR actually counts toward\nyour r\xE9sum\xE9 \u2014 so the list stays honest. Try again after the next refresh.";
1825
1948
  function readIndexCache() {
1826
1949
  try {
1827
- const entry = JSON.parse(readFileSync5(INDEX_CACHE_FILE2, "utf8"));
1950
+ const entry = JSON.parse(readFileSync6(INDEX_CACHE_FILE2, "utf8"));
1828
1951
  if (Date.now() - entry.ts < INDEX_TTL_MS) return entry.index;
1829
1952
  return null;
1830
1953
  } catch {
@@ -1925,7 +2048,7 @@ function displayLanguage(job) {
1925
2048
  const tag = (job.tags ?? []).find((t) => LANGUAGES.has(String(t).toLowerCase()));
1926
2049
  return tag ?? "\u2014";
1927
2050
  }
1928
- function renderRow(i, result) {
2051
+ function renderRow(i, result, claimedIds = /* @__PURE__ */ new Set()) {
1929
2052
  const job = result.job;
1930
2053
  const c = job.contribution ?? {};
1931
2054
  const repo = sanitizeText(c.repoFullName ?? job.company ?? "");
@@ -1934,9 +2057,11 @@ function renderRow(i, result) {
1934
2057
  const lang = sanitizeText(displayLanguage(job));
1935
2058
  const scorePct = `match ${Math.round((result.score ?? 0) * 100)}%`;
1936
2059
  const ref = opportunityShortToken(job.id);
2060
+ const claimed = claimedIds.has(job.id);
2061
+ const badge = claimed ? " \xB7 \u25CF claimed by you" : "";
1937
2062
  const line1 = `${i + 1}. ${linkTitle(job.title, c.issueUrl ?? job.url)} [${ref}]`;
1938
- const line2 = ` ${repo} \xB7 ${num} \xB7 ${label} \xB7 ${lang} \xB7 ${scorePct}`;
1939
- const line3 = ` \u2192 terminalhire claim ${ref}`;
2063
+ const line2 = ` ${repo} \xB7 ${num} \xB7 ${label} \xB7 ${lang} \xB7 ${scorePct}${badge}`;
2064
+ const line3 = claimed ? ` \u2192 claimed by you \u2014 terminalhire claim status ${job.id}` : ` \u2192 terminalhire claim ${ref}`;
1940
2065
  return `${line1}
1941
2066
  ${line2}
1942
2067
  ${line3}`;
@@ -1967,9 +2092,15 @@ async function run(opts = {}) {
1967
2092
  log(EMPTY_STATE);
1968
2093
  return;
1969
2094
  }
2095
+ let claimedIds = /* @__PURE__ */ new Set();
2096
+ try {
2097
+ const { listClaims: listClaims2 } = await Promise.resolve().then(() => (init_claims(), claims_exports));
2098
+ claimedIds = new Set(listClaims2({ active: true }).map((c) => c.id));
2099
+ } catch {
2100
+ }
1970
2101
  log(HEADER);
1971
2102
  log("");
1972
- for (let i = 0; i < results.length; i++) log(renderRow(i, results[i]));
2103
+ for (let i = 0; i < results.length; i++) log(renderRow(i, results[i], claimedIds));
1973
2104
  } catch (err) {
1974
2105
  console.error("terminalhire contribute error:", err?.message ?? err);
1975
2106
  process.exit(1);
@@ -2026,7 +2026,7 @@ function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
2026
2026
  reason: buildReason(details)
2027
2027
  };
2028
2028
  });
2029
- return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => {
2029
+ return scored.filter((r) => r !== null && r.score >= (opts.minScore ?? MIN_SCORE)).sort((a, b) => {
2030
2030
  const byScore = b.score - a.score;
2031
2031
  if (Math.abs(byScore) > TIEBREAK_EPS) return byScore;
2032
2032
  const byAcceptance = (b.acceptance?.count ?? 0) - (a.acceptance?.count ?? 0);
@@ -2602,7 +2602,7 @@ function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
2602
2602
  reason: buildReason(details)
2603
2603
  };
2604
2604
  });
2605
- return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => {
2605
+ return scored.filter((r) => r !== null && r.score >= (opts.minScore ?? MIN_SCORE)).sort((a, b) => {
2606
2606
  const byScore = b.score - a.score;
2607
2607
  if (Math.abs(byScore) > TIEBREAK_EPS) return byScore;
2608
2608
  const byAcceptance = (b.acceptance?.count ?? 0) - (a.acceptance?.count ?? 0);
@@ -10558,22 +10558,146 @@ var init_jpi_project = __esm({
10558
10558
  }
10559
10559
  });
10560
10560
 
10561
+ // src/claims.ts
10562
+ var claims_exports = {};
10563
+ __export(claims_exports, {
10564
+ PUSHED_CLAIM_FIELDS: () => PUSHED_CLAIM_FIELDS,
10565
+ acceptedPRRate: () => acceptedPRRate,
10566
+ findClaim: () => findClaim,
10567
+ listClaims: () => listClaims,
10568
+ readClaims: () => readClaims,
10569
+ recordClaim: () => recordClaim,
10570
+ removeClaim: () => removeClaim,
10571
+ toPushedClaim: () => toPushedClaim,
10572
+ updateClaim: () => updateClaim
10573
+ });
10574
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, renameSync as renameSync4, existsSync as existsSync8 } from "fs";
10575
+ import { join as join13 } from "path";
10576
+ import { homedir as homedir11 } from "os";
10577
+ function toPushedClaim(claim) {
10578
+ return {
10579
+ kind: claim.kind,
10580
+ repoFullName: claim.repoFullName,
10581
+ state: claim.state,
10582
+ prUrl: claim.prUrl,
10583
+ merged: claim.state === "merged",
10584
+ claimedAt: claim.claimedAt,
10585
+ updatedAt: claim.updatedAt
10586
+ };
10587
+ }
10588
+ function nowISO() {
10589
+ return (/* @__PURE__ */ new Date()).toISOString();
10590
+ }
10591
+ function normalizeClaim(c) {
10592
+ return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
10593
+ }
10594
+ function readClaims() {
10595
+ try {
10596
+ if (!existsSync8(CLAIMS_FILE)) return [];
10597
+ const data = JSON.parse(readFileSync13(CLAIMS_FILE, "utf8"));
10598
+ const claims = Array.isArray(data?.claims) ? data.claims : [];
10599
+ return claims.map(normalizeClaim);
10600
+ } catch {
10601
+ return [];
10602
+ }
10603
+ }
10604
+ function writeClaims(claims) {
10605
+ mkdirSync9(TERMINALHIRE_DIR9, { recursive: true });
10606
+ const tmp = `${CLAIMS_FILE}.tmp`;
10607
+ const payload = { claims };
10608
+ writeFileSync9(tmp, JSON.stringify(payload, null, 2), "utf8");
10609
+ renameSync4(tmp, CLAIMS_FILE);
10610
+ }
10611
+ function findClaim(id) {
10612
+ return readClaims().find((c) => c.id === id) ?? null;
10613
+ }
10614
+ function listClaims(opts = {}) {
10615
+ const claims = readClaims();
10616
+ if (!opts.active) return claims;
10617
+ return claims.filter((c) => !TERMINAL_STATES.has(c.state));
10618
+ }
10619
+ function recordClaim(rec) {
10620
+ const claims = readClaims();
10621
+ if (claims.some((c) => c.id === rec.id)) {
10622
+ throw new Error(
10623
+ `claim already exists for '${rec.id}' \u2014 run 'terminalhire claim status ${rec.id}' or 'terminalhire claim release ${rec.id}'`
10624
+ );
10625
+ }
10626
+ const ts = nowISO();
10627
+ const claim = {
10628
+ ...rec,
10629
+ // Defensive default (mirrors normalizeClaim's `kind ?? 'bounty'` pattern):
10630
+ // a caller written before `policy` existed, or a plain-JS caller that skips
10631
+ // it, still produces a valid record instead of `policy: undefined`.
10632
+ policy: rec.policy ?? null,
10633
+ state: "claimed",
10634
+ worktreePath: null,
10635
+ branch: null,
10636
+ prUrl: null,
10637
+ review: null,
10638
+ claimedAt: ts,
10639
+ updatedAt: ts
10640
+ };
10641
+ claims.push(claim);
10642
+ writeClaims(claims);
10643
+ return claim;
10644
+ }
10645
+ function updateClaim(id, patch) {
10646
+ const claims = readClaims();
10647
+ const idx = claims.findIndex((c) => c.id === id);
10648
+ if (idx === -1) return null;
10649
+ claims[idx] = { ...claims[idx], ...patch, updatedAt: nowISO() };
10650
+ writeClaims(claims);
10651
+ return claims[idx];
10652
+ }
10653
+ function removeClaim(id) {
10654
+ const claims = readClaims();
10655
+ const next = claims.filter((c) => c.id !== id);
10656
+ if (next.length === claims.length) return false;
10657
+ writeClaims(next);
10658
+ return true;
10659
+ }
10660
+ function acceptedPRRate(claims = readClaims()) {
10661
+ const total = claims.length;
10662
+ const merged = claims.filter((c) => c.state === "merged").length;
10663
+ return { merged, total, rate: total === 0 ? 0 : merged / total };
10664
+ }
10665
+ var TERMINALHIRE_DIR9, CLAIMS_FILE, PUSHED_CLAIM_FIELDS, TERMINAL_STATES;
10666
+ var init_claims = __esm({
10667
+ "src/claims.ts"() {
10668
+ "use strict";
10669
+ TERMINALHIRE_DIR9 = process.env.TERMINALHIRE_DIR || join13(homedir11(), ".terminalhire");
10670
+ CLAIMS_FILE = join13(TERMINALHIRE_DIR9, "claims.json");
10671
+ PUSHED_CLAIM_FIELDS = [
10672
+ "kind",
10673
+ "repoFullName",
10674
+ "state",
10675
+ "prUrl",
10676
+ "merged",
10677
+ "claimedAt",
10678
+ "updatedAt"
10679
+ ];
10680
+ TERMINAL_STATES = /* @__PURE__ */ new Set(["merged", "abandoned"]);
10681
+ }
10682
+ });
10683
+
10561
10684
  // bin/jpi-bounties.js
10562
10685
  var jpi_bounties_exports = {};
10563
10686
  __export(jpi_bounties_exports, {
10564
10687
  classifyEmptyStatus: () => classifyEmptyStatus,
10565
10688
  filterPaidVisibility: () => filterPaidVisibility,
10566
10689
  getBounties: () => getBounties,
10690
+ printBounty: () => printBounty,
10567
10691
  rankBounties: () => rankBounties,
10568
10692
  run: () => run5
10569
10693
  });
10570
- import { readFileSync as readFileSync13 } from "fs";
10571
- import { join as join13 } from "path";
10572
- import { homedir as homedir11 } from "os";
10694
+ import { readFileSync as readFileSync14 } from "fs";
10695
+ import { join as join14 } from "path";
10696
+ import { homedir as homedir12 } from "os";
10573
10697
  import { createInterface as createInterface6 } from "readline";
10574
10698
  function readIndexCache2() {
10575
10699
  try {
10576
- const entry = JSON.parse(readFileSync13(INDEX_CACHE_FILE3, "utf8"));
10700
+ const entry = JSON.parse(readFileSync14(INDEX_CACHE_FILE3, "utf8"));
10577
10701
  if (Date.now() - entry.ts < INDEX_TTL_MS3) return entry.index;
10578
10702
  return null;
10579
10703
  } catch {
@@ -10604,22 +10728,26 @@ function prompt3(question) {
10604
10728
  function formatAmount(b) {
10605
10729
  return b.amountUSD != null ? "$" + b.amountUSD.toLocaleString() : "$\u2014";
10606
10730
  }
10607
- function printBounty(i, job, score, reason, matchedTags) {
10731
+ function printBounty(i, job, score, reason, matchedTags, claimedIds = /* @__PURE__ */ new Set()) {
10608
10732
  const b = job.bounty ?? {};
10609
10733
  const stars = b.repoStars != null ? ` \xB7 ${b.repoStars}\u2605` : "";
10610
10734
  const effort = b.estimatedEffort ? ` \xB7 ${EFFORT_LABEL[b.estimatedEffort]}` : "";
10611
10735
  const scoreStr = score > 0 ? ` \xB7 match ${Math.round(score * 100)}%` : "";
10612
10736
  const prs = b.competingOpenPRs;
10613
10737
  const contend = prs != null && prs > 0 ? ` \xB7 \u26A0 ${prs} PR${prs === 1 ? "" : "s"} in flight` : "";
10738
+ const claimed = claimedIds.has(job.id);
10739
+ const badge = claimed ? " \xB7 \u25CF claimed by you" : "";
10614
10740
  const ref = opportunityShortToken(job.id);
10615
10741
  console.log(`
10616
10742
  ${i + 1}. ${linkTitle(job.title, job.url)} [${ref}]`);
10617
- console.log(` ${formatAmount(b)}${effort} \xB7 ${sanitizeText(b.repoFullName ?? job.company)}${stars}${scoreStr}${contend}`);
10743
+ console.log(` ${formatAmount(b)}${effort} \xB7 ${sanitizeText(b.repoFullName ?? job.company)}${stars}${scoreStr}${contend}${badge}`);
10618
10744
  if (reason) console.log(` ${reason}`);
10619
10745
  if (matchedTags && matchedTags.length) console.log(` Tags matched: ${matchedTags.slice(0, 5).join(", ")}`);
10620
10746
  console.log(` id: ${job.id}`);
10621
10747
  console.log(` Claim: ${sanitizeText(b.claimUrl ?? job.url)}`);
10622
- console.log(` \u2192 terminalhire claim ${ref}`);
10748
+ console.log(
10749
+ claimed ? ` \u2192 claimed by you \u2014 terminalhire claim status ${job.id}` : ` \u2192 terminalhire claim ${ref}`
10750
+ );
10623
10751
  }
10624
10752
  function rankBounties(bounties, { rankMode = "winnability", scoreOf = () => 0 } = {}) {
10625
10753
  const legacy = rankMode === "legacy";
@@ -10697,6 +10825,12 @@ async function run5() {
10697
10825
  if (result.status !== "ok") return;
10698
10826
  const { bounties, ranked, matchedCount } = result;
10699
10827
  const shown = SHOW_ALL3 ? bounties : bounties.slice(0, LIMIT3);
10828
+ let claimedIds = /* @__PURE__ */ new Set();
10829
+ try {
10830
+ const { listClaims: listClaims2 } = await Promise.resolve().then(() => (init_claims(), claims_exports));
10831
+ claimedIds = new Set(listClaims2({ active: true }).map((c) => c.id));
10832
+ } catch {
10833
+ }
10700
10834
  console.log(
10701
10835
  `
10702
10836
  \u26A1 ${bounties.length} bount${bounties.length === 1 ? "y" : "ies"} you could knock out` + (matchedCount ? ` \u2014 ${matchedCount} matched to your profile` : "") + ` (local rank \u2014 no data sent)
@@ -10704,7 +10838,7 @@ async function run5() {
10704
10838
  );
10705
10839
  for (let i = 0; i < shown.length; i++) {
10706
10840
  const r = ranked.get(shown[i].id);
10707
- printBounty(i, shown[i], r?.score ?? 0, r?.reason, r?.matchedTags);
10841
+ printBounty(i, shown[i], r?.score ?? 0, r?.reason, r?.matchedTags, claimedIds);
10708
10842
  }
10709
10843
  if (!SHOW_ALL3 && bounties.length > shown.length) {
10710
10844
  console.log(`
@@ -10727,15 +10861,15 @@ Open this to claim/work the bounty (you go straight to the source \u2014 we neve
10727
10861
  process.exit(1);
10728
10862
  }
10729
10863
  }
10730
- var TERMINALHIRE_DIR9, INDEX_CACHE_FILE3, INDEX_TTL_MS3, API_URL4, RANK_MODE, DEFAULT_LIMIT3, args4, limitArg3, LIMIT3, PRICED_ONLY, SHOW_ALL3, WINNABLE_ONLY, EFFORT_LABEL;
10864
+ var TERMINALHIRE_DIR10, INDEX_CACHE_FILE3, INDEX_TTL_MS3, API_URL4, RANK_MODE, DEFAULT_LIMIT3, args4, limitArg3, LIMIT3, PRICED_ONLY, SHOW_ALL3, WINNABLE_ONLY, EFFORT_LABEL;
10731
10865
  var init_jpi_bounties = __esm({
10732
10866
  "bin/jpi-bounties.js"() {
10733
10867
  "use strict";
10734
10868
  init_src();
10735
10869
  init_cache_store();
10736
10870
  init_sanitize();
10737
- TERMINALHIRE_DIR9 = process.env.TERMINALHIRE_DIR || join13(homedir11(), ".terminalhire");
10738
- INDEX_CACHE_FILE3 = join13(TERMINALHIRE_DIR9, "index-cache.json");
10871
+ TERMINALHIRE_DIR10 = process.env.TERMINALHIRE_DIR || join14(homedir12(), ".terminalhire");
10872
+ INDEX_CACHE_FILE3 = join14(TERMINALHIRE_DIR10, "index-cache.json");
10739
10873
  INDEX_TTL_MS3 = 15 * 60 * 1e3;
10740
10874
  API_URL4 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
10741
10875
  RANK_MODE = process.env["TERMINALHIRE_BOUNTY_RANK"] ?? "winnability";
@@ -10757,13 +10891,13 @@ __export(jpi_contribute_exports, {
10757
10891
  renderRow: () => renderRow,
10758
10892
  run: () => run6
10759
10893
  });
10760
- import { readFileSync as readFileSync14 } from "fs";
10761
- import { join as join14 } from "path";
10762
- import { homedir as homedir12 } from "os";
10894
+ import { readFileSync as readFileSync15 } from "fs";
10895
+ import { join as join15 } from "path";
10896
+ import { homedir as homedir13 } from "os";
10763
10897
  import { createInterface as createInterface7 } from "readline";
10764
10898
  function readIndexCache3() {
10765
10899
  try {
10766
- const entry = JSON.parse(readFileSync14(INDEX_CACHE_FILE4, "utf8"));
10900
+ const entry = JSON.parse(readFileSync15(INDEX_CACHE_FILE4, "utf8"));
10767
10901
  if (Date.now() - entry.ts < INDEX_TTL_MS4) return entry.index;
10768
10902
  return null;
10769
10903
  } catch {
@@ -10822,7 +10956,7 @@ function displayLanguage(job) {
10822
10956
  const tag = (job.tags ?? []).find((t) => LANGUAGES.has(String(t).toLowerCase()));
10823
10957
  return tag ?? "\u2014";
10824
10958
  }
10825
- function renderRow(i, result) {
10959
+ function renderRow(i, result, claimedIds = /* @__PURE__ */ new Set()) {
10826
10960
  const job = result.job;
10827
10961
  const c = job.contribution ?? {};
10828
10962
  const repo = sanitizeText(c.repoFullName ?? job.company ?? "");
@@ -10831,9 +10965,11 @@ function renderRow(i, result) {
10831
10965
  const lang = sanitizeText(displayLanguage(job));
10832
10966
  const scorePct = `match ${Math.round((result.score ?? 0) * 100)}%`;
10833
10967
  const ref = opportunityShortToken(job.id);
10968
+ const claimed = claimedIds.has(job.id);
10969
+ const badge = claimed ? " \xB7 \u25CF claimed by you" : "";
10834
10970
  const line1 = `${i + 1}. ${linkTitle(job.title, c.issueUrl ?? job.url)} [${ref}]`;
10835
- const line2 = ` ${repo} \xB7 ${num} \xB7 ${label} \xB7 ${lang} \xB7 ${scorePct}`;
10836
- const line3 = ` \u2192 terminalhire claim ${ref}`;
10971
+ const line2 = ` ${repo} \xB7 ${num} \xB7 ${label} \xB7 ${lang} \xB7 ${scorePct}${badge}`;
10972
+ const line3 = claimed ? ` \u2192 claimed by you \u2014 terminalhire claim status ${job.id}` : ` \u2192 terminalhire claim ${ref}`;
10837
10973
  return `${line1}
10838
10974
  ${line2}
10839
10975
  ${line3}`;
@@ -10864,15 +11000,21 @@ async function run6(opts = {}) {
10864
11000
  log(EMPTY_STATE);
10865
11001
  return;
10866
11002
  }
11003
+ let claimedIds = /* @__PURE__ */ new Set();
11004
+ try {
11005
+ const { listClaims: listClaims2 } = await Promise.resolve().then(() => (init_claims(), claims_exports));
11006
+ claimedIds = new Set(listClaims2({ active: true }).map((c) => c.id));
11007
+ } catch {
11008
+ }
10867
11009
  log(HEADER);
10868
11010
  log("");
10869
- for (let i = 0; i < results.length; i++) log(renderRow(i, results[i]));
11011
+ for (let i = 0; i < results.length; i++) log(renderRow(i, results[i], claimedIds));
10870
11012
  } catch (err) {
10871
11013
  console.error("terminalhire contribute error:", err?.message ?? err);
10872
11014
  process.exit(1);
10873
11015
  }
10874
11016
  }
10875
- var TERMINALHIRE_DIR10, INDEX_CACHE_FILE4, INDEX_TTL_MS4, API_URL5, HEADER, OPT_IN_PROMPT, EMPTY_STATE, STRONG_THRESHOLD, LANGUAGES;
11017
+ var TERMINALHIRE_DIR11, INDEX_CACHE_FILE4, INDEX_TTL_MS4, API_URL5, HEADER, OPT_IN_PROMPT, EMPTY_STATE, STRONG_THRESHOLD, LANGUAGES;
10876
11018
  var init_jpi_contribute = __esm({
10877
11019
  "bin/jpi-contribute.js"() {
10878
11020
  "use strict";
@@ -10880,8 +11022,8 @@ var init_jpi_contribute = __esm({
10880
11022
  init_cache_store();
10881
11023
  init_config();
10882
11024
  init_sanitize();
10883
- TERMINALHIRE_DIR10 = process.env.TERMINALHIRE_DIR || join14(homedir12(), ".terminalhire");
10884
- INDEX_CACHE_FILE4 = join14(TERMINALHIRE_DIR10, "index-cache.json");
11025
+ TERMINALHIRE_DIR11 = process.env.TERMINALHIRE_DIR || join15(homedir13(), ".terminalhire");
11026
+ INDEX_CACHE_FILE4 = join15(TERMINALHIRE_DIR11, "index-cache.json");
10885
11027
  INDEX_TTL_MS4 = 15 * 60 * 1e3;
10886
11028
  API_URL5 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
10887
11029
  HEADER = "Contribution opportunities \u2014 open issues where a merged PR actually counts toward your r\xE9sum\xE9.\nRepos \u226550\u2605 / \u226510 contributors \xB7 unassigned \xB7 merit-merge \xB7 matched locally to your stack.";
@@ -10932,129 +11074,6 @@ var init_jpi_contribute = __esm({
10932
11074
  }
10933
11075
  });
10934
11076
 
10935
- // src/claims.ts
10936
- var claims_exports = {};
10937
- __export(claims_exports, {
10938
- PUSHED_CLAIM_FIELDS: () => PUSHED_CLAIM_FIELDS,
10939
- acceptedPRRate: () => acceptedPRRate,
10940
- findClaim: () => findClaim,
10941
- listClaims: () => listClaims,
10942
- readClaims: () => readClaims,
10943
- recordClaim: () => recordClaim,
10944
- removeClaim: () => removeClaim,
10945
- toPushedClaim: () => toPushedClaim,
10946
- updateClaim: () => updateClaim
10947
- });
10948
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, renameSync as renameSync4, existsSync as existsSync8 } from "fs";
10949
- import { join as join15 } from "path";
10950
- import { homedir as homedir13 } from "os";
10951
- function toPushedClaim(claim) {
10952
- return {
10953
- kind: claim.kind,
10954
- repoFullName: claim.repoFullName,
10955
- state: claim.state,
10956
- prUrl: claim.prUrl,
10957
- merged: claim.state === "merged",
10958
- claimedAt: claim.claimedAt,
10959
- updatedAt: claim.updatedAt
10960
- };
10961
- }
10962
- function nowISO() {
10963
- return (/* @__PURE__ */ new Date()).toISOString();
10964
- }
10965
- function normalizeClaim(c) {
10966
- return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
10967
- }
10968
- function readClaims() {
10969
- try {
10970
- if (!existsSync8(CLAIMS_FILE)) return [];
10971
- const data = JSON.parse(readFileSync15(CLAIMS_FILE, "utf8"));
10972
- const claims = Array.isArray(data?.claims) ? data.claims : [];
10973
- return claims.map(normalizeClaim);
10974
- } catch {
10975
- return [];
10976
- }
10977
- }
10978
- function writeClaims(claims) {
10979
- mkdirSync9(TERMINALHIRE_DIR11, { recursive: true });
10980
- const tmp = `${CLAIMS_FILE}.tmp`;
10981
- const payload = { claims };
10982
- writeFileSync9(tmp, JSON.stringify(payload, null, 2), "utf8");
10983
- renameSync4(tmp, CLAIMS_FILE);
10984
- }
10985
- function findClaim(id) {
10986
- return readClaims().find((c) => c.id === id) ?? null;
10987
- }
10988
- function listClaims(opts = {}) {
10989
- const claims = readClaims();
10990
- if (!opts.active) return claims;
10991
- return claims.filter((c) => !TERMINAL_STATES.has(c.state));
10992
- }
10993
- function recordClaim(rec) {
10994
- const claims = readClaims();
10995
- if (claims.some((c) => c.id === rec.id)) {
10996
- throw new Error(
10997
- `claim already exists for '${rec.id}' \u2014 run 'terminalhire claim status ${rec.id}' or 'terminalhire claim release ${rec.id}'`
10998
- );
10999
- }
11000
- const ts = nowISO();
11001
- const claim = {
11002
- ...rec,
11003
- // Defensive default (mirrors normalizeClaim's `kind ?? 'bounty'` pattern):
11004
- // a caller written before `policy` existed, or a plain-JS caller that skips
11005
- // it, still produces a valid record instead of `policy: undefined`.
11006
- policy: rec.policy ?? null,
11007
- state: "claimed",
11008
- worktreePath: null,
11009
- branch: null,
11010
- prUrl: null,
11011
- review: null,
11012
- claimedAt: ts,
11013
- updatedAt: ts
11014
- };
11015
- claims.push(claim);
11016
- writeClaims(claims);
11017
- return claim;
11018
- }
11019
- function updateClaim(id, patch) {
11020
- const claims = readClaims();
11021
- const idx = claims.findIndex((c) => c.id === id);
11022
- if (idx === -1) return null;
11023
- claims[idx] = { ...claims[idx], ...patch, updatedAt: nowISO() };
11024
- writeClaims(claims);
11025
- return claims[idx];
11026
- }
11027
- function removeClaim(id) {
11028
- const claims = readClaims();
11029
- const next = claims.filter((c) => c.id !== id);
11030
- if (next.length === claims.length) return false;
11031
- writeClaims(next);
11032
- return true;
11033
- }
11034
- function acceptedPRRate(claims = readClaims()) {
11035
- const total = claims.length;
11036
- const merged = claims.filter((c) => c.state === "merged").length;
11037
- return { merged, total, rate: total === 0 ? 0 : merged / total };
11038
- }
11039
- var TERMINALHIRE_DIR11, CLAIMS_FILE, PUSHED_CLAIM_FIELDS, TERMINAL_STATES;
11040
- var init_claims = __esm({
11041
- "src/claims.ts"() {
11042
- "use strict";
11043
- TERMINALHIRE_DIR11 = process.env.TERMINALHIRE_DIR || join15(homedir13(), ".terminalhire");
11044
- CLAIMS_FILE = join15(TERMINALHIRE_DIR11, "claims.json");
11045
- PUSHED_CLAIM_FIELDS = [
11046
- "kind",
11047
- "repoFullName",
11048
- "state",
11049
- "prUrl",
11050
- "merged",
11051
- "claimedAt",
11052
- "updatedAt"
11053
- ];
11054
- TERMINAL_STATES = /* @__PURE__ */ new Set(["merged", "abandoned"]);
11055
- }
11056
- });
11057
-
11058
11077
  // bin/claim-push-bg.js
11059
11078
  var claim_push_bg_exports = {};
11060
11079
  __export(claim_push_bg_exports, {
@@ -409,6 +409,43 @@ var init_tui_core = __esm({
409
409
  }
410
410
  });
411
411
 
412
+ // src/claims.ts
413
+ import { readFileSync, writeFileSync, mkdirSync, renameSync, existsSync } from "fs";
414
+ import { join } from "path";
415
+ import { homedir } from "os";
416
+ function normalizeClaim(c) {
417
+ return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
418
+ }
419
+ function readClaims() {
420
+ try {
421
+ if (!existsSync(CLAIMS_FILE)) return [];
422
+ const data = JSON.parse(readFileSync(CLAIMS_FILE, "utf8"));
423
+ const claims = Array.isArray(data?.claims) ? data.claims : [];
424
+ return claims.map(normalizeClaim);
425
+ } catch {
426
+ return [];
427
+ }
428
+ }
429
+ function listClaims(opts = {}) {
430
+ const claims = readClaims();
431
+ if (!opts.active) return claims;
432
+ return claims.filter((c) => !TERMINAL_STATES.has(c.state));
433
+ }
434
+ function acceptedPRRate(claims = readClaims()) {
435
+ const total = claims.length;
436
+ const merged = claims.filter((c) => c.state === "merged").length;
437
+ return { merged, total, rate: total === 0 ? 0 : merged / total };
438
+ }
439
+ var TERMINALHIRE_DIR, CLAIMS_FILE, TERMINAL_STATES;
440
+ var init_claims = __esm({
441
+ "src/claims.ts"() {
442
+ "use strict";
443
+ TERMINALHIRE_DIR = process.env.TERMINALHIRE_DIR || join(homedir(), ".terminalhire");
444
+ CLAIMS_FILE = join(TERMINALHIRE_DIR, "claims.json");
445
+ TERMINAL_STATES = /* @__PURE__ */ new Set(["merged", "abandoned"]);
446
+ }
447
+ });
448
+
412
449
  // ../../packages/core/src/types.ts
413
450
  function isBounty(job) {
414
451
  return job.source === "bounty" && job.bounty != null;
@@ -2397,7 +2434,7 @@ function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
2397
2434
  reason: buildReason(details)
2398
2435
  };
2399
2436
  });
2400
- return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => {
2437
+ return scored.filter((r) => r !== null && r.score >= (opts.minScore ?? MIN_SCORE)).sort((a, b) => {
2401
2438
  const byScore = b.score - a.score;
2402
2439
  if (Math.abs(byScore) > TIEBREAK_EPS) return byScore;
2403
2440
  const byAcceptance = (b.acceptance?.count ?? 0) - (a.acceptance?.count ?? 0);
@@ -9418,39 +9455,7 @@ var init_intro2 = __esm({
9418
9455
 
9419
9456
  // bin/jpi-hub.js
9420
9457
  init_tui_core();
9421
-
9422
- // src/claims.ts
9423
- import { readFileSync, writeFileSync, mkdirSync, renameSync, existsSync } from "fs";
9424
- import { join } from "path";
9425
- import { homedir } from "os";
9426
- var TERMINALHIRE_DIR = process.env.TERMINALHIRE_DIR || join(homedir(), ".terminalhire");
9427
- var CLAIMS_FILE = join(TERMINALHIRE_DIR, "claims.json");
9428
- var TERMINAL_STATES = /* @__PURE__ */ new Set(["merged", "abandoned"]);
9429
- function normalizeClaim(c) {
9430
- return { ...c, kind: c.kind ?? "bounty", policy: c.policy ?? null };
9431
- }
9432
- function readClaims() {
9433
- try {
9434
- if (!existsSync(CLAIMS_FILE)) return [];
9435
- const data = JSON.parse(readFileSync(CLAIMS_FILE, "utf8"));
9436
- const claims = Array.isArray(data?.claims) ? data.claims : [];
9437
- return claims.map(normalizeClaim);
9438
- } catch {
9439
- return [];
9440
- }
9441
- }
9442
- function listClaims(opts = {}) {
9443
- const claims = readClaims();
9444
- if (!opts.active) return claims;
9445
- return claims.filter((c) => !TERMINAL_STATES.has(c.state));
9446
- }
9447
- function acceptedPRRate(claims = readClaims()) {
9448
- const total = claims.length;
9449
- const merged = claims.filter((c) => c.state === "merged").length;
9450
- return { merged, total, rate: total === 0 ? 0 : merged / total };
9451
- }
9452
-
9453
- // bin/jpi-hub.js
9458
+ init_claims();
9454
9459
  init_profile();
9455
9460
  init_config();
9456
9461
  init_jpi_chat_read();
@@ -1997,7 +1997,7 @@ function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
1997
1997
  reason: buildReason(details)
1998
1998
  };
1999
1999
  });
2000
- return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => {
2000
+ return scored.filter((r) => r !== null && r.score >= (opts.minScore ?? MIN_SCORE)).sort((a, b) => {
2001
2001
  const byScore = b.score - a.score;
2002
2002
  if (Math.abs(byScore) > TIEBREAK_EPS) return byScore;
2003
2003
  const byAcceptance = (b.acceptance?.count ?? 0) - (a.acceptance?.count ?? 0);
@@ -2026,7 +2026,7 @@ function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
2026
2026
  reason: buildReason(details)
2027
2027
  };
2028
2028
  });
2029
- return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => {
2029
+ return scored.filter((r) => r !== null && r.score >= (opts.minScore ?? MIN_SCORE)).sort((a, b) => {
2030
2030
  const byScore = b.score - a.score;
2031
2031
  if (Math.abs(byScore) > TIEBREAK_EPS) return byScore;
2032
2032
  const byAcceptance = (b.acceptance?.count ?? 0) - (a.acceptance?.count ?? 0);
@@ -2331,7 +2331,7 @@ function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
2331
2331
  reason: buildReason(details)
2332
2332
  };
2333
2333
  });
2334
- return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => {
2334
+ return scored.filter((r) => r !== null && r.score >= (opts.minScore ?? MIN_SCORE)).sort((a, b) => {
2335
2335
  const byScore = b.score - a.score;
2336
2336
  if (Math.abs(byScore) > TIEBREAK_EPS) return byScore;
2337
2337
  const byAcceptance = (b.acceptance?.count ?? 0) - (a.acceptance?.count ?? 0);
@@ -1997,7 +1997,7 @@ function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
1997
1997
  reason: buildReason(details)
1998
1998
  };
1999
1999
  });
2000
- return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => {
2000
+ return scored.filter((r) => r !== null && r.score >= (opts.minScore ?? MIN_SCORE)).sort((a, b) => {
2001
2001
  const byScore = b.score - a.score;
2002
2002
  if (Math.abs(byScore) > TIEBREAK_EPS) return byScore;
2003
2003
  const byAcceptance = (b.acceptance?.count ?? 0) - (a.acceptance?.count ?? 0);
@@ -2116,7 +2116,7 @@ function match(fp, jobs, limit = 5, now = Date.now(), opts = {}) {
2116
2116
  reason: buildReason(details)
2117
2117
  };
2118
2118
  });
2119
- return scored.filter((r) => r !== null && r.score >= MIN_SCORE).sort((a, b) => {
2119
+ return scored.filter((r) => r !== null && r.score >= (opts.minScore ?? MIN_SCORE)).sort((a, b) => {
2120
2120
  const byScore = b.score - a.score;
2121
2121
  if (Math.abs(byScore) > TIEBREAK_EPS) return byScore;
2122
2122
  const byAcceptance = (b.acceptance?.count ?? 0) - (a.acceptance?.count ?? 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminalhire",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
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",