terminalhire 0.4.4 → 0.4.6

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.
@@ -1640,6 +1640,28 @@ var init_bounty_gate = __esm({
1640
1640
  }
1641
1641
  });
1642
1642
 
1643
+ // ../../packages/core/src/concurrency.ts
1644
+ async function mapWithConcurrency(items, limit, fn) {
1645
+ const results = new Array(items.length);
1646
+ if (items.length === 0) return results;
1647
+ const workers = Math.max(1, Math.min(Math.floor(limit) || 1, items.length));
1648
+ let next = 0;
1649
+ async function run2() {
1650
+ for (; ; ) {
1651
+ const i = next++;
1652
+ if (i >= items.length) return;
1653
+ results[i] = await fn(items[i], i);
1654
+ }
1655
+ }
1656
+ await Promise.all(Array.from({ length: workers }, run2));
1657
+ return results;
1658
+ }
1659
+ var init_concurrency = __esm({
1660
+ "../../packages/core/src/concurrency.ts"() {
1661
+ "use strict";
1662
+ }
1663
+ });
1664
+
1643
1665
  // ../../packages/core/src/feeds/github-bounties.ts
1644
1666
  function authHeaders() {
1645
1667
  const token = process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"];
@@ -1731,7 +1753,7 @@ async function fetchRepoBounties(repoFullName) {
1731
1753
  if (!issues) return [];
1732
1754
  const bounties = issues.filter(isBountyIssue).slice(0, MAX_BOUNTIES_PER_REPO);
1733
1755
  const owner = repo.owner.login;
1734
- return Promise.all(bounties.map(async (issue) => {
1756
+ return mapWithConcurrency(bounties, BOUNTY_FETCH_CONCURRENCY, async (issue) => {
1735
1757
  const title = decodeEntities(issue.title).trim();
1736
1758
  const body = issue.body ? decodeEntities(issue.body) : "";
1737
1759
  const amountUSD = parseAmountUSD(title) ?? parseAmountUSD(body) ?? await fetchCommentAmount(repoFullName, issue.number);
@@ -1760,7 +1782,7 @@ async function fetchRepoBounties(repoFullName) {
1760
1782
  },
1761
1783
  raw: issue
1762
1784
  };
1763
- }));
1785
+ });
1764
1786
  }
1765
1787
  function repoFullNameFromApiUrl(url) {
1766
1788
  const m = url.match(/\/repos\/([^/]+)\/([^/]+)\/?$/);
@@ -1902,7 +1924,7 @@ async function fetchSearchBounties() {
1902
1924
  }
1903
1925
  return jobs;
1904
1926
  }
1905
- var GITHUB_API, BOUNTY_LABEL_RE, SEARCH_QUERIES, SEARCH_PER_PAGE, MAX_SEARCH_BOUNTIES, MAX_SEARCH_ISSUES_SCANNED, REPO_META_CONCURRENCY, repoMetaCache, MAX_PR_PAGES, repoOpenPRRefsCache, issueStateCache, githubBounties;
1927
+ var GITHUB_API, BOUNTY_LABEL_RE, SEARCH_QUERIES, SEARCH_PER_PAGE, MAX_SEARCH_BOUNTIES, MAX_SEARCH_ISSUES_SCANNED, REPO_META_CONCURRENCY, BOUNTY_FETCH_CONCURRENCY, repoMetaCache, MAX_PR_PAGES, repoOpenPRRefsCache, issueStateCache, githubBounties;
1906
1928
  var init_github_bounties = __esm({
1907
1929
  "../../packages/core/src/feeds/github-bounties.ts"() {
1908
1930
  "use strict";
@@ -1910,6 +1932,7 @@ var init_github_bounties = __esm({
1910
1932
  init_entities();
1911
1933
  init_bounty_gate();
1912
1934
  init_http();
1935
+ init_concurrency();
1913
1936
  GITHUB_API = "https://api.github.com";
1914
1937
  BOUNTY_LABEL_RE = /bounty|reward|funded|💎|💰/i;
1915
1938
  SEARCH_QUERIES = [
@@ -1922,6 +1945,7 @@ var init_github_bounties = __esm({
1922
1945
  MAX_SEARCH_BOUNTIES = 150;
1923
1946
  MAX_SEARCH_ISSUES_SCANNED = 300;
1924
1947
  REPO_META_CONCURRENCY = 15;
1948
+ BOUNTY_FETCH_CONCURRENCY = 6;
1925
1949
  repoMetaCache = /* @__PURE__ */ new Map();
1926
1950
  MAX_PR_PAGES = 3;
1927
1951
  repoOpenPRRefsCache = /* @__PURE__ */ new Map();
@@ -143,7 +143,7 @@ ${p.body ?? ""}`)).length;
143
143
  return null;
144
144
  }
145
145
  }
146
- async function fetchIssueState(repoFullName, issueNumber) {
146
+ async function fetchIssue(repoFullName, issueNumber) {
147
147
  try {
148
148
  const res = await fetch(`${GH_API}/repos/${repoFullName}/issues/${issueNumber}`, {
149
149
  headers: GH_HEADERS,
@@ -151,7 +151,8 @@ async function fetchIssueState(repoFullName, issueNumber) {
151
151
  });
152
152
  if (!res.ok) return null;
153
153
  const issue = await res.json();
154
- return issue.state === "open" ? "open" : issue.state === "closed" ? "closed" : null;
154
+ const state = issue.state === "open" ? "open" : issue.state === "closed" ? "closed" : null;
155
+ return { state, title: typeof issue.title === "string" ? issue.title : null };
155
156
  } catch {
156
157
  return null;
157
158
  }
@@ -179,14 +180,7 @@ function printMetric(rate) {
179
180
  console.log(`
180
181
  \u{1F4CA} Accepted-PR rate: ${rate.merged}/${rate.total} claims merged (${pct}%)`);
181
182
  }
182
- async function cmdRecord(arg) {
183
- const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
184
- if (!arg) {
185
- console.error("Usage: terminalhire claim record <bountyId|issueUrl>");
186
- console.error(" Run `terminalhire bounties` first to populate the local index cache,");
187
- console.error(" then pass the id shown in its output \u2014 or pass a GitHub issue URL directly.");
188
- process.exit(1);
189
- }
183
+ async function resolveBounty(arg) {
190
184
  let bountyId, title, repoFullName, issueUrl, amountUSD;
191
185
  const job = findBountyInCache(arg);
192
186
  if (job) {
@@ -198,11 +192,7 @@ async function cmdRecord(arg) {
198
192
  amountUSD = b.amountUSD ?? null;
199
193
  } else {
200
194
  const parsed = parseGitHubUrl(arg);
201
- if (!parsed) {
202
- console.error(`terminalhire claim: '${arg}' is not in the index cache and is not a GitHub issue URL.`);
203
- console.error(" Run `terminalhire bounties` to populate the cache, or pass a full issue URL.");
204
- process.exit(1);
205
- }
195
+ if (!parsed) return null;
206
196
  bountyId = `gh:${parsed.repoFullName}#${parsed.number}`;
207
197
  title = `${parsed.repoFullName}#${parsed.number}`;
208
198
  repoFullName = parsed.repoFullName;
@@ -210,14 +200,40 @@ async function cmdRecord(arg) {
210
200
  amountUSD = null;
211
201
  }
212
202
  const ghIssue = parseGitHubUrl(issueUrl);
213
- const [issueState, openPRs] = ghIssue ? await Promise.all([
214
- fetchIssueState(repoFullName, ghIssue.number),
203
+ const [issue, openPRs] = ghIssue ? await Promise.all([
204
+ fetchIssue(repoFullName, ghIssue.number),
215
205
  countOpenPRsReferencingIssue(repoFullName, ghIssue.number)
216
- // Guardrail #5
217
206
  ]) : [null, null];
218
- if (issueState === "closed") {
207
+ const issueState = issue ? issue.state : null;
208
+ if (!job && issue && issue.title) title = issue.title;
209
+ return {
210
+ bountyId,
211
+ title,
212
+ repoFullName,
213
+ issueUrl,
214
+ amountUSD,
215
+ issueState,
216
+ openPRs,
217
+ issueNumber: ghIssue ? ghIssue.number : null
218
+ };
219
+ }
220
+ async function cmdRecord(arg) {
221
+ const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
222
+ if (!arg) {
223
+ console.error("Usage: terminalhire claim record <bountyId|issueUrl>");
224
+ console.error(" Run `terminalhire bounties` first to populate the local index cache,");
225
+ console.error(" then pass the id shown in its output \u2014 or pass a GitHub issue URL directly.");
226
+ process.exit(1);
227
+ }
228
+ const b = await resolveBounty(arg);
229
+ if (!b) {
230
+ console.error(`terminalhire claim: '${arg}' is not in the index cache and is not a GitHub issue URL.`);
231
+ console.error(" Run `terminalhire bounties` to populate the cache, or pass a full issue URL.");
232
+ process.exit(1);
233
+ }
234
+ if (b.issueState === "closed") {
219
235
  console.error(
220
- `terminalhire claim: ${repoFullName}#${ghIssue.number} is CLOSED \u2014 not claimable.
236
+ `terminalhire claim: ${b.repoFullName}#${b.issueNumber} is CLOSED \u2014 not claimable.
221
237
  The bounty index drops closed issues; this one is likely a stale cache entry.
222
238
  Run \`terminalhire bounties\` for the current open pool.`
223
239
  );
@@ -225,7 +241,7 @@ async function cmdRecord(arg) {
225
241
  }
226
242
  let claim;
227
243
  try {
228
- claim = claims.recordClaim({ id: bountyId, bountyId, title, repoFullName, issueUrl, amountUSD, openPRsAtClaim: openPRs });
244
+ claim = claims.recordClaim({ id: b.bountyId, bountyId: b.bountyId, title: b.title, repoFullName: b.repoFullName, issueUrl: b.issueUrl, amountUSD: b.amountUSD, openPRsAtClaim: b.openPRs });
229
245
  } catch (err) {
230
246
  console.error(`terminalhire claim: ${err.message ?? err}`);
231
247
  process.exit(1);
@@ -236,10 +252,10 @@ async function cmdRecord(arg) {
236
252
  console.log(` repo: ${claim.repoFullName}`);
237
253
  console.log(` amount: ${fmtAmount(claim.amountUSD)}`);
238
254
  console.log(` issue: ${claim.issueUrl}`);
239
- if (openPRs == null) {
255
+ if (b.openPRs == null) {
240
256
  console.log(" open PRs: unknown (GitHub read unavailable \u2014 check the issue manually before working)");
241
- } else if (openPRs > 0) {
242
- console.log(` \u26A0 open PRs referencing this issue: ${openPRs} \u2014 someone may already be on it. Check before working.`);
257
+ } else if (b.openPRs > 0) {
258
+ console.log(` \u26A0 open PRs referencing this issue: ${b.openPRs} \u2014 someone may already be on it. Check before working.`);
243
259
  } else {
244
260
  console.log(" open PRs referencing this issue: 0");
245
261
  }
@@ -250,6 +266,49 @@ async function cmdRecord(arg) {
250
266
  console.log(" \u2022 no access to ~/.terminalhire (the executor never needs your profile)");
251
267
  console.log("\n Next: do the work, then `terminalhire claim update " + claim.id + " <state>` as you progress.");
252
268
  }
269
+ async function cmdPreview(arg, { json } = {}) {
270
+ if (!arg) {
271
+ console.error("Usage: terminalhire claim preview <bountyId|issueUrl> [--json]");
272
+ process.exit(1);
273
+ }
274
+ const b = await resolveBounty(arg);
275
+ if (!b) {
276
+ console.error(`terminalhire claim: '${arg}' is not in the index cache and is not a GitHub issue URL.`);
277
+ console.error(" Run `terminalhire bounties` to populate the cache, or pass a full issue URL.");
278
+ process.exit(1);
279
+ }
280
+ if (json) {
281
+ process.stdout.write(
282
+ JSON.stringify({
283
+ bountyId: b.bountyId,
284
+ title: b.title,
285
+ amountUSD: b.amountUSD,
286
+ repoFullName: b.repoFullName,
287
+ issueUrl: b.issueUrl,
288
+ issueState: b.issueState,
289
+ openPRs: b.openPRs
290
+ }) + "\n"
291
+ );
292
+ return;
293
+ }
294
+ console.log(`
295
+ BOUNTY \xB7 ${b.title}`);
296
+ console.log(` id: ${b.bountyId}`);
297
+ console.log(` repo: ${b.repoFullName}`);
298
+ console.log(` amount: ${fmtAmount(b.amountUSD)}`);
299
+ console.log(` issue: ${b.issueUrl}`);
300
+ if (b.issueState === "closed") {
301
+ console.log(" \u2717 CLOSED \u2014 not claimable (the pool drops closed issues; likely a stale cache entry)");
302
+ }
303
+ if (b.openPRs == null) {
304
+ console.log(" open PRs: unknown (GitHub read unavailable \u2014 check the issue manually before working)");
305
+ } else if (b.openPRs > 0) {
306
+ console.log(` \u26A0 open PRs referencing this issue: ${b.openPRs} \u2014 someone may already be on it. Check before working.`);
307
+ } else {
308
+ console.log(" open PRs referencing this issue: 0");
309
+ }
310
+ console.log("\n Preview only \u2014 NOT claimed. Run `terminalhire claim record " + arg + "` to claim it.");
311
+ }
253
312
  async function cmdList(active) {
254
313
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
255
314
  const list = claims.listClaims({ active });
@@ -335,8 +394,12 @@ async function run() {
335
394
  const verb = process.argv[2];
336
395
  const rest = process.argv.slice(3).filter((a) => !a.startsWith("--"));
337
396
  const active = process.argv.includes("--active");
397
+ const json = process.argv.includes("--json");
338
398
  try {
339
399
  switch (verb) {
400
+ case "preview":
401
+ await cmdPreview(rest[0], { json });
402
+ break;
340
403
  case "record":
341
404
  await cmdRecord(rest[0]);
342
405
  break;
@@ -353,7 +416,7 @@ async function run() {
353
416
  await cmdRelease(rest[0]);
354
417
  break;
355
418
  default:
356
- console.error(`terminalhire claim: unknown verb '${verb ?? ""}'. Expected: record | list | status | update | release`);
419
+ console.error(`terminalhire claim: unknown verb '${verb ?? ""}'. Expected: preview | record | list | status | update | release`);
357
420
  process.exit(1);
358
421
  }
359
422
  } catch (err) {
@@ -9,6 +9,35 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/open-url.js
13
+ import { spawn } from "child_process";
14
+ function openInBrowser(url) {
15
+ let cmd;
16
+ let args3;
17
+ if (process.platform === "darwin") {
18
+ cmd = "open";
19
+ args3 = [url];
20
+ } else if (process.platform === "win32") {
21
+ cmd = "cmd";
22
+ args3 = ["/c", "start", "", url];
23
+ } else {
24
+ cmd = "xdg-open";
25
+ args3 = [url];
26
+ }
27
+ try {
28
+ const child = spawn(cmd, args3, { stdio: "ignore", detached: true });
29
+ child.on("error", () => {
30
+ });
31
+ child.unref();
32
+ } catch {
33
+ }
34
+ }
35
+ var init_open_url = __esm({
36
+ "src/open-url.js"() {
37
+ "use strict";
38
+ }
39
+ });
40
+
12
41
  // src/github-auth.ts
13
42
  var github_auth_exports = {};
14
43
  __export(github_auth_exports, {
@@ -1854,6 +1883,28 @@ var init_bounty_gate = __esm({
1854
1883
  }
1855
1884
  });
1856
1885
 
1886
+ // ../../packages/core/src/concurrency.ts
1887
+ async function mapWithConcurrency(items, limit, fn) {
1888
+ const results = new Array(items.length);
1889
+ if (items.length === 0) return results;
1890
+ const workers = Math.max(1, Math.min(Math.floor(limit) || 1, items.length));
1891
+ let next = 0;
1892
+ async function run13() {
1893
+ for (; ; ) {
1894
+ const i = next++;
1895
+ if (i >= items.length) return;
1896
+ results[i] = await fn(items[i], i);
1897
+ }
1898
+ }
1899
+ await Promise.all(Array.from({ length: workers }, run13));
1900
+ return results;
1901
+ }
1902
+ var init_concurrency = __esm({
1903
+ "../../packages/core/src/concurrency.ts"() {
1904
+ "use strict";
1905
+ }
1906
+ });
1907
+
1857
1908
  // ../../packages/core/src/feeds/github-bounties.ts
1858
1909
  function authHeaders() {
1859
1910
  const token = process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"];
@@ -1945,7 +1996,7 @@ async function fetchRepoBounties(repoFullName) {
1945
1996
  if (!issues) return [];
1946
1997
  const bounties = issues.filter(isBountyIssue).slice(0, MAX_BOUNTIES_PER_REPO);
1947
1998
  const owner = repo.owner.login;
1948
- return Promise.all(bounties.map(async (issue) => {
1999
+ return mapWithConcurrency(bounties, BOUNTY_FETCH_CONCURRENCY, async (issue) => {
1949
2000
  const title = decodeEntities(issue.title).trim();
1950
2001
  const body = issue.body ? decodeEntities(issue.body) : "";
1951
2002
  const amountUSD = parseAmountUSD(title) ?? parseAmountUSD(body) ?? await fetchCommentAmount(repoFullName, issue.number);
@@ -1974,7 +2025,7 @@ async function fetchRepoBounties(repoFullName) {
1974
2025
  },
1975
2026
  raw: issue
1976
2027
  };
1977
- }));
2028
+ });
1978
2029
  }
1979
2030
  function repoFullNameFromApiUrl(url) {
1980
2031
  const m = url.match(/\/repos\/([^/]+)\/([^/]+)\/?$/);
@@ -2116,7 +2167,7 @@ async function fetchSearchBounties() {
2116
2167
  }
2117
2168
  return jobs;
2118
2169
  }
2119
- var GITHUB_API, BOUNTY_LABEL_RE, SEARCH_QUERIES, SEARCH_PER_PAGE, MAX_SEARCH_BOUNTIES, MAX_SEARCH_ISSUES_SCANNED, REPO_META_CONCURRENCY, repoMetaCache, MAX_PR_PAGES, repoOpenPRRefsCache, issueStateCache, githubBounties;
2170
+ var GITHUB_API, BOUNTY_LABEL_RE, SEARCH_QUERIES, SEARCH_PER_PAGE, MAX_SEARCH_BOUNTIES, MAX_SEARCH_ISSUES_SCANNED, REPO_META_CONCURRENCY, BOUNTY_FETCH_CONCURRENCY, repoMetaCache, MAX_PR_PAGES, repoOpenPRRefsCache, issueStateCache, githubBounties;
2120
2171
  var init_github_bounties = __esm({
2121
2172
  "../../packages/core/src/feeds/github-bounties.ts"() {
2122
2173
  "use strict";
@@ -2124,6 +2175,7 @@ var init_github_bounties = __esm({
2124
2175
  init_entities();
2125
2176
  init_bounty_gate();
2126
2177
  init_http();
2178
+ init_concurrency();
2127
2179
  GITHUB_API = "https://api.github.com";
2128
2180
  BOUNTY_LABEL_RE = /bounty|reward|funded|💎|💰/i;
2129
2181
  SEARCH_QUERIES = [
@@ -2136,6 +2188,7 @@ var init_github_bounties = __esm({
2136
2188
  MAX_SEARCH_BOUNTIES = 150;
2137
2189
  MAX_SEARCH_ISSUES_SCANNED = 300;
2138
2190
  REPO_META_CONCURRENCY = 15;
2191
+ BOUNTY_FETCH_CONCURRENCY = 6;
2139
2192
  repoMetaCache = /* @__PURE__ */ new Map();
2140
2193
  MAX_PR_PAGES = 3;
2141
2194
  repoOpenPRRefsCache = /* @__PURE__ */ new Map();
@@ -3124,6 +3177,26 @@ async function runLogin() {
3124
3177
  console.log(" Profile updated at ~/.terminalhire/profile.enc (encrypted at rest)");
3125
3178
  console.log(" GitHub data stays on your machine unless you consent to share it in a lead.");
3126
3179
  console.log("");
3180
+ const skipWeb = process.argv.includes("--no-web");
3181
+ if (!isMock && !skipWeb) {
3182
+ try {
3183
+ const OAUTH_BASE = "https://www.terminalhire.com";
3184
+ const webUrl = `${OAUTH_BASE}/api/auth/github?next=/dashboard`;
3185
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
3186
+ console.log(" Your web profile & r\xE9sum\xE9 \u2014 public GitHub data only,");
3187
+ console.log(" your local profile is NOT uploaded.");
3188
+ console.log(` \u2192 ${webUrl}`);
3189
+ if (process.stdout.isTTY) {
3190
+ console.log(" Opening it now to sign you in at terminalhire.com\u2026");
3191
+ openInBrowser(webUrl);
3192
+ } else {
3193
+ console.log(" Open the link above to sign in & view your r\xE9sum\xE9.");
3194
+ }
3195
+ console.log(" (skip next time with: terminalhire login --no-web)");
3196
+ console.log("");
3197
+ } catch {
3198
+ }
3199
+ }
3127
3200
  console.log(" Run `terminalhire jobs` to see matching roles using your enriched profile.");
3128
3201
  console.log("");
3129
3202
  } catch (err) {
@@ -3161,6 +3234,7 @@ async function runLogout() {
3161
3234
  var init_jpi_login = __esm({
3162
3235
  "bin/jpi-login.js"() {
3163
3236
  "use strict";
3237
+ init_open_url();
3164
3238
  }
3165
3239
  });
3166
3240
 
@@ -3694,7 +3768,7 @@ ${p.body ?? ""}`)).length;
3694
3768
  return null;
3695
3769
  }
3696
3770
  }
3697
- async function fetchIssueState2(repoFullName, issueNumber) {
3771
+ async function fetchIssue(repoFullName, issueNumber) {
3698
3772
  try {
3699
3773
  const res = await fetch(`${GH_API}/repos/${repoFullName}/issues/${issueNumber}`, {
3700
3774
  headers: GH_HEADERS,
@@ -3702,7 +3776,8 @@ async function fetchIssueState2(repoFullName, issueNumber) {
3702
3776
  });
3703
3777
  if (!res.ok) return null;
3704
3778
  const issue = await res.json();
3705
- return issue.state === "open" ? "open" : issue.state === "closed" ? "closed" : null;
3779
+ const state = issue.state === "open" ? "open" : issue.state === "closed" ? "closed" : null;
3780
+ return { state, title: typeof issue.title === "string" ? issue.title : null };
3706
3781
  } catch {
3707
3782
  return null;
3708
3783
  }
@@ -3730,14 +3805,7 @@ function printMetric(rate) {
3730
3805
  console.log(`
3731
3806
  \u{1F4CA} Accepted-PR rate: ${rate.merged}/${rate.total} claims merged (${pct}%)`);
3732
3807
  }
3733
- async function cmdRecord(arg) {
3734
- const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
3735
- if (!arg) {
3736
- console.error("Usage: terminalhire claim record <bountyId|issueUrl>");
3737
- console.error(" Run `terminalhire bounties` first to populate the local index cache,");
3738
- console.error(" then pass the id shown in its output \u2014 or pass a GitHub issue URL directly.");
3739
- process.exit(1);
3740
- }
3808
+ async function resolveBounty(arg) {
3741
3809
  let bountyId, title, repoFullName, issueUrl, amountUSD;
3742
3810
  const job = findBountyInCache(arg);
3743
3811
  if (job) {
@@ -3749,11 +3817,7 @@ async function cmdRecord(arg) {
3749
3817
  amountUSD = b.amountUSD ?? null;
3750
3818
  } else {
3751
3819
  const parsed = parseGitHubUrl(arg);
3752
- if (!parsed) {
3753
- console.error(`terminalhire claim: '${arg}' is not in the index cache and is not a GitHub issue URL.`);
3754
- console.error(" Run `terminalhire bounties` to populate the cache, or pass a full issue URL.");
3755
- process.exit(1);
3756
- }
3820
+ if (!parsed) return null;
3757
3821
  bountyId = `gh:${parsed.repoFullName}#${parsed.number}`;
3758
3822
  title = `${parsed.repoFullName}#${parsed.number}`;
3759
3823
  repoFullName = parsed.repoFullName;
@@ -3761,14 +3825,40 @@ async function cmdRecord(arg) {
3761
3825
  amountUSD = null;
3762
3826
  }
3763
3827
  const ghIssue = parseGitHubUrl(issueUrl);
3764
- const [issueState, openPRs] = ghIssue ? await Promise.all([
3765
- fetchIssueState2(repoFullName, ghIssue.number),
3828
+ const [issue, openPRs] = ghIssue ? await Promise.all([
3829
+ fetchIssue(repoFullName, ghIssue.number),
3766
3830
  countOpenPRsReferencingIssue(repoFullName, ghIssue.number)
3767
- // Guardrail #5
3768
3831
  ]) : [null, null];
3769
- if (issueState === "closed") {
3832
+ const issueState = issue ? issue.state : null;
3833
+ if (!job && issue && issue.title) title = issue.title;
3834
+ return {
3835
+ bountyId,
3836
+ title,
3837
+ repoFullName,
3838
+ issueUrl,
3839
+ amountUSD,
3840
+ issueState,
3841
+ openPRs,
3842
+ issueNumber: ghIssue ? ghIssue.number : null
3843
+ };
3844
+ }
3845
+ async function cmdRecord(arg) {
3846
+ const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
3847
+ if (!arg) {
3848
+ console.error("Usage: terminalhire claim record <bountyId|issueUrl>");
3849
+ console.error(" Run `terminalhire bounties` first to populate the local index cache,");
3850
+ console.error(" then pass the id shown in its output \u2014 or pass a GitHub issue URL directly.");
3851
+ process.exit(1);
3852
+ }
3853
+ const b = await resolveBounty(arg);
3854
+ if (!b) {
3855
+ console.error(`terminalhire claim: '${arg}' is not in the index cache and is not a GitHub issue URL.`);
3856
+ console.error(" Run `terminalhire bounties` to populate the cache, or pass a full issue URL.");
3857
+ process.exit(1);
3858
+ }
3859
+ if (b.issueState === "closed") {
3770
3860
  console.error(
3771
- `terminalhire claim: ${repoFullName}#${ghIssue.number} is CLOSED \u2014 not claimable.
3861
+ `terminalhire claim: ${b.repoFullName}#${b.issueNumber} is CLOSED \u2014 not claimable.
3772
3862
  The bounty index drops closed issues; this one is likely a stale cache entry.
3773
3863
  Run \`terminalhire bounties\` for the current open pool.`
3774
3864
  );
@@ -3776,7 +3866,7 @@ async function cmdRecord(arg) {
3776
3866
  }
3777
3867
  let claim;
3778
3868
  try {
3779
- claim = claims.recordClaim({ id: bountyId, bountyId, title, repoFullName, issueUrl, amountUSD, openPRsAtClaim: openPRs });
3869
+ claim = claims.recordClaim({ id: b.bountyId, bountyId: b.bountyId, title: b.title, repoFullName: b.repoFullName, issueUrl: b.issueUrl, amountUSD: b.amountUSD, openPRsAtClaim: b.openPRs });
3780
3870
  } catch (err) {
3781
3871
  console.error(`terminalhire claim: ${err.message ?? err}`);
3782
3872
  process.exit(1);
@@ -3787,10 +3877,10 @@ async function cmdRecord(arg) {
3787
3877
  console.log(` repo: ${claim.repoFullName}`);
3788
3878
  console.log(` amount: ${fmtAmount(claim.amountUSD)}`);
3789
3879
  console.log(` issue: ${claim.issueUrl}`);
3790
- if (openPRs == null) {
3880
+ if (b.openPRs == null) {
3791
3881
  console.log(" open PRs: unknown (GitHub read unavailable \u2014 check the issue manually before working)");
3792
- } else if (openPRs > 0) {
3793
- console.log(` \u26A0 open PRs referencing this issue: ${openPRs} \u2014 someone may already be on it. Check before working.`);
3882
+ } else if (b.openPRs > 0) {
3883
+ console.log(` \u26A0 open PRs referencing this issue: ${b.openPRs} \u2014 someone may already be on it. Check before working.`);
3794
3884
  } else {
3795
3885
  console.log(" open PRs referencing this issue: 0");
3796
3886
  }
@@ -3801,6 +3891,49 @@ async function cmdRecord(arg) {
3801
3891
  console.log(" \u2022 no access to ~/.terminalhire (the executor never needs your profile)");
3802
3892
  console.log("\n Next: do the work, then `terminalhire claim update " + claim.id + " <state>` as you progress.");
3803
3893
  }
3894
+ async function cmdPreview(arg, { json } = {}) {
3895
+ if (!arg) {
3896
+ console.error("Usage: terminalhire claim preview <bountyId|issueUrl> [--json]");
3897
+ process.exit(1);
3898
+ }
3899
+ const b = await resolveBounty(arg);
3900
+ if (!b) {
3901
+ console.error(`terminalhire claim: '${arg}' is not in the index cache and is not a GitHub issue URL.`);
3902
+ console.error(" Run `terminalhire bounties` to populate the cache, or pass a full issue URL.");
3903
+ process.exit(1);
3904
+ }
3905
+ if (json) {
3906
+ process.stdout.write(
3907
+ JSON.stringify({
3908
+ bountyId: b.bountyId,
3909
+ title: b.title,
3910
+ amountUSD: b.amountUSD,
3911
+ repoFullName: b.repoFullName,
3912
+ issueUrl: b.issueUrl,
3913
+ issueState: b.issueState,
3914
+ openPRs: b.openPRs
3915
+ }) + "\n"
3916
+ );
3917
+ return;
3918
+ }
3919
+ console.log(`
3920
+ BOUNTY \xB7 ${b.title}`);
3921
+ console.log(` id: ${b.bountyId}`);
3922
+ console.log(` repo: ${b.repoFullName}`);
3923
+ console.log(` amount: ${fmtAmount(b.amountUSD)}`);
3924
+ console.log(` issue: ${b.issueUrl}`);
3925
+ if (b.issueState === "closed") {
3926
+ console.log(" \u2717 CLOSED \u2014 not claimable (the pool drops closed issues; likely a stale cache entry)");
3927
+ }
3928
+ if (b.openPRs == null) {
3929
+ console.log(" open PRs: unknown (GitHub read unavailable \u2014 check the issue manually before working)");
3930
+ } else if (b.openPRs > 0) {
3931
+ console.log(` \u26A0 open PRs referencing this issue: ${b.openPRs} \u2014 someone may already be on it. Check before working.`);
3932
+ } else {
3933
+ console.log(" open PRs referencing this issue: 0");
3934
+ }
3935
+ console.log("\n Preview only \u2014 NOT claimed. Run `terminalhire claim record " + arg + "` to claim it.");
3936
+ }
3804
3937
  async function cmdList(active) {
3805
3938
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
3806
3939
  const list = claims.listClaims({ active });
@@ -3886,8 +4019,12 @@ async function run4() {
3886
4019
  const verb = process.argv[2];
3887
4020
  const rest = process.argv.slice(3).filter((a) => !a.startsWith("--"));
3888
4021
  const active = process.argv.includes("--active");
4022
+ const json = process.argv.includes("--json");
3889
4023
  try {
3890
4024
  switch (verb) {
4025
+ case "preview":
4026
+ await cmdPreview(rest[0], { json });
4027
+ break;
3891
4028
  case "record":
3892
4029
  await cmdRecord(rest[0]);
3893
4030
  break;
@@ -3904,7 +4041,7 @@ async function run4() {
3904
4041
  await cmdRelease(rest[0]);
3905
4042
  break;
3906
4043
  default:
3907
- console.error(`terminalhire claim: unknown verb '${verb ?? ""}'. Expected: record | list | status | update | release`);
4044
+ console.error(`terminalhire claim: unknown verb '${verb ?? ""}'. Expected: preview | record | list | status | update | release`);
3908
4045
  process.exit(1);
3909
4046
  }
3910
4047
  } catch (err) {
@@ -4886,7 +5023,6 @@ import { readFileSync as readFileSync12, writeFileSync as writeFileSync9, mkdirS
4886
5023
  import { join as join12 } from "path";
4887
5024
  import { homedir as homedir10, hostname as osHostname } from "os";
4888
5025
  import { createInterface as createInterface5 } from "readline";
4889
- import { spawn } from "child_process";
4890
5026
  function ask2(question) {
4891
5027
  const rl = createInterface5({ input: process.stdin, output: process.stdout });
4892
5028
  return new Promise((res) => {
@@ -4955,27 +5091,6 @@ function renderPreview(fields) {
4955
5091
  console.log(" This is NOT required to use terminalhire.");
4956
5092
  console.log("");
4957
5093
  }
4958
- function openInBrowser(url) {
4959
- let cmd;
4960
- let args3;
4961
- if (process.platform === "darwin") {
4962
- cmd = "open";
4963
- args3 = [url];
4964
- } else if (process.platform === "win32") {
4965
- cmd = "cmd";
4966
- args3 = ["/c", "start", "", url];
4967
- } else {
4968
- cmd = "xdg-open";
4969
- args3 = [url];
4970
- }
4971
- try {
4972
- const child = spawn(cmd, args3, { stdio: "ignore", detached: true });
4973
- child.on("error", () => {
4974
- });
4975
- child.unref();
4976
- } catch {
4977
- }
4978
- }
4979
5094
  function sleep2(ms) {
4980
5095
  return new Promise((res) => setTimeout(res, ms));
4981
5096
  }
@@ -5244,6 +5359,7 @@ var TH_DIR3, TIER1_MARKER, API_URL3, SYNC_BASE, POLL_INTERVAL_MS, POLL_TIMEOUT_M
5244
5359
  var init_jpi_sync = __esm({
5245
5360
  "bin/jpi-sync.js"() {
5246
5361
  "use strict";
5362
+ init_open_url();
5247
5363
  TH_DIR3 = process.env["TERMINALHIRE_DIR"] || join12(homedir10(), ".terminalhire");
5248
5364
  TIER1_MARKER = join12(TH_DIR3, "tier1.json");
5249
5365
  API_URL3 = process.env["TERMINALHIRE_API_URL"] || process.env["JPI_API_URL"] || "https://terminalhire.com";
@@ -1640,6 +1640,28 @@ var init_bounty_gate = __esm({
1640
1640
  }
1641
1641
  });
1642
1642
 
1643
+ // ../../packages/core/src/concurrency.ts
1644
+ async function mapWithConcurrency(items, limit, fn) {
1645
+ const results = new Array(items.length);
1646
+ if (items.length === 0) return results;
1647
+ const workers = Math.max(1, Math.min(Math.floor(limit) || 1, items.length));
1648
+ let next = 0;
1649
+ async function run2() {
1650
+ for (; ; ) {
1651
+ const i = next++;
1652
+ if (i >= items.length) return;
1653
+ results[i] = await fn(items[i], i);
1654
+ }
1655
+ }
1656
+ await Promise.all(Array.from({ length: workers }, run2));
1657
+ return results;
1658
+ }
1659
+ var init_concurrency = __esm({
1660
+ "../../packages/core/src/concurrency.ts"() {
1661
+ "use strict";
1662
+ }
1663
+ });
1664
+
1643
1665
  // ../../packages/core/src/feeds/github-bounties.ts
1644
1666
  function authHeaders() {
1645
1667
  const token = process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"];
@@ -1731,7 +1753,7 @@ async function fetchRepoBounties(repoFullName) {
1731
1753
  if (!issues) return [];
1732
1754
  const bounties = issues.filter(isBountyIssue).slice(0, MAX_BOUNTIES_PER_REPO);
1733
1755
  const owner = repo.owner.login;
1734
- return Promise.all(bounties.map(async (issue) => {
1756
+ return mapWithConcurrency(bounties, BOUNTY_FETCH_CONCURRENCY, async (issue) => {
1735
1757
  const title = decodeEntities(issue.title).trim();
1736
1758
  const body = issue.body ? decodeEntities(issue.body) : "";
1737
1759
  const amountUSD = parseAmountUSD(title) ?? parseAmountUSD(body) ?? await fetchCommentAmount(repoFullName, issue.number);
@@ -1760,7 +1782,7 @@ async function fetchRepoBounties(repoFullName) {
1760
1782
  },
1761
1783
  raw: issue
1762
1784
  };
1763
- }));
1785
+ });
1764
1786
  }
1765
1787
  function repoFullNameFromApiUrl(url) {
1766
1788
  const m = url.match(/\/repos\/([^/]+)\/([^/]+)\/?$/);
@@ -1902,7 +1924,7 @@ async function fetchSearchBounties() {
1902
1924
  }
1903
1925
  return jobs;
1904
1926
  }
1905
- var GITHUB_API, BOUNTY_LABEL_RE, SEARCH_QUERIES, SEARCH_PER_PAGE, MAX_SEARCH_BOUNTIES, MAX_SEARCH_ISSUES_SCANNED, REPO_META_CONCURRENCY, repoMetaCache, MAX_PR_PAGES, repoOpenPRRefsCache, issueStateCache, githubBounties;
1927
+ var GITHUB_API, BOUNTY_LABEL_RE, SEARCH_QUERIES, SEARCH_PER_PAGE, MAX_SEARCH_BOUNTIES, MAX_SEARCH_ISSUES_SCANNED, REPO_META_CONCURRENCY, BOUNTY_FETCH_CONCURRENCY, repoMetaCache, MAX_PR_PAGES, repoOpenPRRefsCache, issueStateCache, githubBounties;
1906
1928
  var init_github_bounties = __esm({
1907
1929
  "../../packages/core/src/feeds/github-bounties.ts"() {
1908
1930
  "use strict";
@@ -1910,6 +1932,7 @@ var init_github_bounties = __esm({
1910
1932
  init_entities();
1911
1933
  init_bounty_gate();
1912
1934
  init_http();
1935
+ init_concurrency();
1913
1936
  GITHUB_API = "https://api.github.com";
1914
1937
  BOUNTY_LABEL_RE = /bounty|reward|funded|💎|💰/i;
1915
1938
  SEARCH_QUERIES = [
@@ -1922,6 +1945,7 @@ var init_github_bounties = __esm({
1922
1945
  MAX_SEARCH_BOUNTIES = 150;
1923
1946
  MAX_SEARCH_ISSUES_SCANNED = 300;
1924
1947
  REPO_META_CONCURRENCY = 15;
1948
+ BOUNTY_FETCH_CONCURRENCY = 6;
1925
1949
  repoMetaCache = /* @__PURE__ */ new Map();
1926
1950
  MAX_PR_PAGES = 3;
1927
1951
  repoOpenPRRefsCache = /* @__PURE__ */ new Map();
@@ -509,6 +509,13 @@ var init_bounty_gate = __esm({
509
509
  }
510
510
  });
511
511
 
512
+ // ../../packages/core/src/concurrency.ts
513
+ var init_concurrency = __esm({
514
+ "../../packages/core/src/concurrency.ts"() {
515
+ "use strict";
516
+ }
517
+ });
518
+
512
519
  // ../../packages/core/src/feeds/github-bounties.ts
513
520
  var init_github_bounties = __esm({
514
521
  "../../packages/core/src/feeds/github-bounties.ts"() {
@@ -517,6 +524,7 @@ var init_github_bounties = __esm({
517
524
  init_entities();
518
525
  init_bounty_gate();
519
526
  init_http();
527
+ init_concurrency();
520
528
  }
521
529
  });
522
530
 
@@ -1854,6 +1854,28 @@ var init_bounty_gate = __esm({
1854
1854
  }
1855
1855
  });
1856
1856
 
1857
+ // ../../packages/core/src/concurrency.ts
1858
+ async function mapWithConcurrency(items, limit, fn) {
1859
+ const results = new Array(items.length);
1860
+ if (items.length === 0) return results;
1861
+ const workers = Math.max(1, Math.min(Math.floor(limit) || 1, items.length));
1862
+ let next = 0;
1863
+ async function run2() {
1864
+ for (; ; ) {
1865
+ const i = next++;
1866
+ if (i >= items.length) return;
1867
+ results[i] = await fn(items[i], i);
1868
+ }
1869
+ }
1870
+ await Promise.all(Array.from({ length: workers }, run2));
1871
+ return results;
1872
+ }
1873
+ var init_concurrency = __esm({
1874
+ "../../packages/core/src/concurrency.ts"() {
1875
+ "use strict";
1876
+ }
1877
+ });
1878
+
1857
1879
  // ../../packages/core/src/feeds/github-bounties.ts
1858
1880
  function authHeaders() {
1859
1881
  const token = process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"];
@@ -1945,7 +1967,7 @@ async function fetchRepoBounties(repoFullName) {
1945
1967
  if (!issues) return [];
1946
1968
  const bounties = issues.filter(isBountyIssue).slice(0, MAX_BOUNTIES_PER_REPO);
1947
1969
  const owner = repo.owner.login;
1948
- return Promise.all(bounties.map(async (issue) => {
1970
+ return mapWithConcurrency(bounties, BOUNTY_FETCH_CONCURRENCY, async (issue) => {
1949
1971
  const title = decodeEntities(issue.title).trim();
1950
1972
  const body = issue.body ? decodeEntities(issue.body) : "";
1951
1973
  const amountUSD = parseAmountUSD(title) ?? parseAmountUSD(body) ?? await fetchCommentAmount(repoFullName, issue.number);
@@ -1974,7 +1996,7 @@ async function fetchRepoBounties(repoFullName) {
1974
1996
  },
1975
1997
  raw: issue
1976
1998
  };
1977
- }));
1999
+ });
1978
2000
  }
1979
2001
  function repoFullNameFromApiUrl(url) {
1980
2002
  const m = url.match(/\/repos\/([^/]+)\/([^/]+)\/?$/);
@@ -2116,7 +2138,7 @@ async function fetchSearchBounties() {
2116
2138
  }
2117
2139
  return jobs;
2118
2140
  }
2119
- var GITHUB_API, BOUNTY_LABEL_RE, SEARCH_QUERIES, SEARCH_PER_PAGE, MAX_SEARCH_BOUNTIES, MAX_SEARCH_ISSUES_SCANNED, REPO_META_CONCURRENCY, repoMetaCache, MAX_PR_PAGES, repoOpenPRRefsCache, issueStateCache, githubBounties;
2141
+ var GITHUB_API, BOUNTY_LABEL_RE, SEARCH_QUERIES, SEARCH_PER_PAGE, MAX_SEARCH_BOUNTIES, MAX_SEARCH_ISSUES_SCANNED, REPO_META_CONCURRENCY, BOUNTY_FETCH_CONCURRENCY, repoMetaCache, MAX_PR_PAGES, repoOpenPRRefsCache, issueStateCache, githubBounties;
2120
2142
  var init_github_bounties = __esm({
2121
2143
  "../../packages/core/src/feeds/github-bounties.ts"() {
2122
2144
  "use strict";
@@ -2124,6 +2146,7 @@ var init_github_bounties = __esm({
2124
2146
  init_entities();
2125
2147
  init_bounty_gate();
2126
2148
  init_http();
2149
+ init_concurrency();
2127
2150
  GITHUB_API = "https://api.github.com";
2128
2151
  BOUNTY_LABEL_RE = /bounty|reward|funded|💎|💰/i;
2129
2152
  SEARCH_QUERIES = [
@@ -2136,6 +2159,7 @@ var init_github_bounties = __esm({
2136
2159
  MAX_SEARCH_BOUNTIES = 150;
2137
2160
  MAX_SEARCH_ISSUES_SCANNED = 300;
2138
2161
  REPO_META_CONCURRENCY = 15;
2162
+ BOUNTY_FETCH_CONCURRENCY = 6;
2139
2163
  repoMetaCache = /* @__PURE__ */ new Map();
2140
2164
  MAX_PR_PAGES = 3;
2141
2165
  repoOpenPRRefsCache = /* @__PURE__ */ new Map();
@@ -3035,6 +3059,30 @@ var init_profile = __esm({
3035
3059
  }
3036
3060
  });
3037
3061
 
3062
+ // src/open-url.js
3063
+ import { spawn } from "child_process";
3064
+ function openInBrowser(url) {
3065
+ let cmd;
3066
+ let args;
3067
+ if (process.platform === "darwin") {
3068
+ cmd = "open";
3069
+ args = [url];
3070
+ } else if (process.platform === "win32") {
3071
+ cmd = "cmd";
3072
+ args = ["/c", "start", "", url];
3073
+ } else {
3074
+ cmd = "xdg-open";
3075
+ args = [url];
3076
+ }
3077
+ try {
3078
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
3079
+ child.on("error", () => {
3080
+ });
3081
+ child.unref();
3082
+ } catch {
3083
+ }
3084
+ }
3085
+
3038
3086
  // bin/jpi-login.js
3039
3087
  async function run() {
3040
3088
  const subcommand = process.argv[2];
@@ -3120,6 +3168,26 @@ async function runLogin() {
3120
3168
  console.log(" Profile updated at ~/.terminalhire/profile.enc (encrypted at rest)");
3121
3169
  console.log(" GitHub data stays on your machine unless you consent to share it in a lead.");
3122
3170
  console.log("");
3171
+ const skipWeb = process.argv.includes("--no-web");
3172
+ if (!isMock && !skipWeb) {
3173
+ try {
3174
+ const OAUTH_BASE = "https://www.terminalhire.com";
3175
+ const webUrl = `${OAUTH_BASE}/api/auth/github?next=/dashboard`;
3176
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
3177
+ console.log(" Your web profile & r\xE9sum\xE9 \u2014 public GitHub data only,");
3178
+ console.log(" your local profile is NOT uploaded.");
3179
+ console.log(` \u2192 ${webUrl}`);
3180
+ if (process.stdout.isTTY) {
3181
+ console.log(" Opening it now to sign you in at terminalhire.com\u2026");
3182
+ openInBrowser(webUrl);
3183
+ } else {
3184
+ console.log(" Open the link above to sign in & view your r\xE9sum\xE9.");
3185
+ }
3186
+ console.log(" (skip next time with: terminalhire login --no-web)");
3187
+ console.log("");
3188
+ } catch {
3189
+ }
3190
+ }
3123
3191
  console.log(" Run `terminalhire jobs` to see matching roles using your enriched profile.");
3124
3192
  console.log("");
3125
3193
  } catch (err) {
@@ -509,6 +509,13 @@ var init_bounty_gate = __esm({
509
509
  }
510
510
  });
511
511
 
512
+ // ../../packages/core/src/concurrency.ts
513
+ var init_concurrency = __esm({
514
+ "../../packages/core/src/concurrency.ts"() {
515
+ "use strict";
516
+ }
517
+ });
518
+
512
519
  // ../../packages/core/src/feeds/github-bounties.ts
513
520
  var init_github_bounties = __esm({
514
521
  "../../packages/core/src/feeds/github-bounties.ts"() {
@@ -517,6 +524,7 @@ var init_github_bounties = __esm({
517
524
  init_entities();
518
525
  init_bounty_gate();
519
526
  init_http();
527
+ init_concurrency();
520
528
  }
521
529
  });
522
530
 
@@ -1640,6 +1640,28 @@ var init_bounty_gate = __esm({
1640
1640
  }
1641
1641
  });
1642
1642
 
1643
+ // ../../packages/core/src/concurrency.ts
1644
+ async function mapWithConcurrency(items, limit, fn) {
1645
+ const results = new Array(items.length);
1646
+ if (items.length === 0) return results;
1647
+ const workers = Math.max(1, Math.min(Math.floor(limit) || 1, items.length));
1648
+ let next = 0;
1649
+ async function run2() {
1650
+ for (; ; ) {
1651
+ const i = next++;
1652
+ if (i >= items.length) return;
1653
+ results[i] = await fn(items[i], i);
1654
+ }
1655
+ }
1656
+ await Promise.all(Array.from({ length: workers }, run2));
1657
+ return results;
1658
+ }
1659
+ var init_concurrency = __esm({
1660
+ "../../packages/core/src/concurrency.ts"() {
1661
+ "use strict";
1662
+ }
1663
+ });
1664
+
1643
1665
  // ../../packages/core/src/feeds/github-bounties.ts
1644
1666
  function authHeaders() {
1645
1667
  const token = process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"];
@@ -1731,7 +1753,7 @@ async function fetchRepoBounties(repoFullName) {
1731
1753
  if (!issues) return [];
1732
1754
  const bounties = issues.filter(isBountyIssue).slice(0, MAX_BOUNTIES_PER_REPO);
1733
1755
  const owner = repo.owner.login;
1734
- return Promise.all(bounties.map(async (issue) => {
1756
+ return mapWithConcurrency(bounties, BOUNTY_FETCH_CONCURRENCY, async (issue) => {
1735
1757
  const title = decodeEntities(issue.title).trim();
1736
1758
  const body = issue.body ? decodeEntities(issue.body) : "";
1737
1759
  const amountUSD = parseAmountUSD(title) ?? parseAmountUSD(body) ?? await fetchCommentAmount(repoFullName, issue.number);
@@ -1760,7 +1782,7 @@ async function fetchRepoBounties(repoFullName) {
1760
1782
  },
1761
1783
  raw: issue
1762
1784
  };
1763
- }));
1785
+ });
1764
1786
  }
1765
1787
  function repoFullNameFromApiUrl(url) {
1766
1788
  const m = url.match(/\/repos\/([^/]+)\/([^/]+)\/?$/);
@@ -1902,7 +1924,7 @@ async function fetchSearchBounties() {
1902
1924
  }
1903
1925
  return jobs;
1904
1926
  }
1905
- var GITHUB_API, BOUNTY_LABEL_RE, SEARCH_QUERIES, SEARCH_PER_PAGE, MAX_SEARCH_BOUNTIES, MAX_SEARCH_ISSUES_SCANNED, REPO_META_CONCURRENCY, repoMetaCache, MAX_PR_PAGES, repoOpenPRRefsCache, issueStateCache, githubBounties;
1927
+ var GITHUB_API, BOUNTY_LABEL_RE, SEARCH_QUERIES, SEARCH_PER_PAGE, MAX_SEARCH_BOUNTIES, MAX_SEARCH_ISSUES_SCANNED, REPO_META_CONCURRENCY, BOUNTY_FETCH_CONCURRENCY, repoMetaCache, MAX_PR_PAGES, repoOpenPRRefsCache, issueStateCache, githubBounties;
1906
1928
  var init_github_bounties = __esm({
1907
1929
  "../../packages/core/src/feeds/github-bounties.ts"() {
1908
1930
  "use strict";
@@ -1910,6 +1932,7 @@ var init_github_bounties = __esm({
1910
1932
  init_entities();
1911
1933
  init_bounty_gate();
1912
1934
  init_http();
1935
+ init_concurrency();
1913
1936
  GITHUB_API = "https://api.github.com";
1914
1937
  BOUNTY_LABEL_RE = /bounty|reward|funded|💎|💰/i;
1915
1938
  SEARCH_QUERIES = [
@@ -1922,6 +1945,7 @@ var init_github_bounties = __esm({
1922
1945
  MAX_SEARCH_BOUNTIES = 150;
1923
1946
  MAX_SEARCH_ISSUES_SCANNED = 300;
1924
1947
  REPO_META_CONCURRENCY = 15;
1948
+ BOUNTY_FETCH_CONCURRENCY = 6;
1925
1949
  repoMetaCache = /* @__PURE__ */ new Map();
1926
1950
  MAX_PR_PAGES = 3;
1927
1951
  repoOpenPRRefsCache = /* @__PURE__ */ new Map();
@@ -509,6 +509,13 @@ var init_bounty_gate = __esm({
509
509
  }
510
510
  });
511
511
 
512
+ // ../../packages/core/src/concurrency.ts
513
+ var init_concurrency = __esm({
514
+ "../../packages/core/src/concurrency.ts"() {
515
+ "use strict";
516
+ }
517
+ });
518
+
512
519
  // ../../packages/core/src/feeds/github-bounties.ts
513
520
  var init_github_bounties = __esm({
514
521
  "../../packages/core/src/feeds/github-bounties.ts"() {
@@ -517,6 +524,7 @@ var init_github_bounties = __esm({
517
524
  init_entities();
518
525
  init_bounty_gate();
519
526
  init_http();
527
+ init_concurrency();
520
528
  }
521
529
  });
522
530
 
@@ -509,6 +509,13 @@ var init_bounty_gate = __esm({
509
509
  }
510
510
  });
511
511
 
512
+ // ../../packages/core/src/concurrency.ts
513
+ var init_concurrency = __esm({
514
+ "../../packages/core/src/concurrency.ts"() {
515
+ "use strict";
516
+ }
517
+ });
518
+
512
519
  // ../../packages/core/src/feeds/github-bounties.ts
513
520
  var init_github_bounties = __esm({
514
521
  "../../packages/core/src/feeds/github-bounties.ts"() {
@@ -517,6 +524,7 @@ var init_github_bounties = __esm({
517
524
  init_entities();
518
525
  init_bounty_gate();
519
526
  init_http();
527
+ init_concurrency();
520
528
  }
521
529
  });
522
530
 
@@ -954,7 +962,32 @@ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSy
954
962
  import { join as join3 } from "path";
955
963
  import { homedir as homedir2, hostname as osHostname } from "os";
956
964
  import { createInterface } from "readline";
965
+
966
+ // src/open-url.js
957
967
  import { spawn } from "child_process";
968
+ function openInBrowser(url) {
969
+ let cmd;
970
+ let args;
971
+ if (process.platform === "darwin") {
972
+ cmd = "open";
973
+ args = [url];
974
+ } else if (process.platform === "win32") {
975
+ cmd = "cmd";
976
+ args = ["/c", "start", "", url];
977
+ } else {
978
+ cmd = "xdg-open";
979
+ args = [url];
980
+ }
981
+ try {
982
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
983
+ child.on("error", () => {
984
+ });
985
+ child.unref();
986
+ } catch {
987
+ }
988
+ }
989
+
990
+ // bin/jpi-sync.js
958
991
  var TH_DIR = process.env["TERMINALHIRE_DIR"] || join3(homedir2(), ".terminalhire");
959
992
  var TIER1_MARKER = join3(TH_DIR, "tier1.json");
960
993
  var API_URL = process.env["TERMINALHIRE_API_URL"] || process.env["JPI_API_URL"] || "https://terminalhire.com";
@@ -1030,27 +1063,6 @@ function renderPreview(fields) {
1030
1063
  console.log(" This is NOT required to use terminalhire.");
1031
1064
  console.log("");
1032
1065
  }
1033
- function openInBrowser(url) {
1034
- let cmd;
1035
- let args;
1036
- if (process.platform === "darwin") {
1037
- cmd = "open";
1038
- args = [url];
1039
- } else if (process.platform === "win32") {
1040
- cmd = "cmd";
1041
- args = ["/c", "start", "", url];
1042
- } else {
1043
- cmd = "xdg-open";
1044
- args = [url];
1045
- }
1046
- try {
1047
- const child = spawn(cmd, args, { stdio: "ignore", detached: true });
1048
- child.on("error", () => {
1049
- });
1050
- child.unref();
1051
- } catch {
1052
- }
1053
- }
1054
1066
  function sleep(ms) {
1055
1067
  return new Promise((res) => setTimeout(res, ms));
1056
1068
  }
@@ -0,0 +1,49 @@
1
+ // src/acceptance-score.ts
2
+ var clamp01 = (n) => Math.max(0, Math.min(1, n));
3
+ function scoreDiffAcceptance(input) {
4
+ const reasons = [];
5
+ let score = 0.5;
6
+ const prs = Math.max(0, Math.floor(input.competingOpenPRs));
7
+ if (prs === 0) {
8
+ score += 0.2;
9
+ reasons.push("no competing open PRs (+0.20)");
10
+ } else if (prs === 1) {
11
+ score -= 0.05;
12
+ reasons.push("1 competing open PR (-0.05)");
13
+ } else if (prs === 2) {
14
+ score -= 0.2;
15
+ reasons.push("2 competing open PRs (-0.20)");
16
+ } else {
17
+ score -= 0.35;
18
+ reasons.push(`${prs} competing open PRs \u2014 heavily contested (-0.35)`);
19
+ }
20
+ if (input.filesChanged <= 3 && input.linesChanged <= 150) {
21
+ score += 0.15;
22
+ reasons.push("small, focused diff (+0.15)");
23
+ } else if (input.filesChanged > 15 || input.linesChanged > 800) {
24
+ score -= 0.2;
25
+ reasons.push("large diff \u2014 harder to review/merge (-0.20)");
26
+ } else {
27
+ reasons.push("moderate diff size (0)");
28
+ }
29
+ if (input.touchesTests) {
30
+ score += 0.1;
31
+ reasons.push("includes test changes (+0.10)");
32
+ } else {
33
+ score -= 0.05;
34
+ reasons.push("no test changes (-0.05)");
35
+ }
36
+ if (input.matchesIssueArea === true) {
37
+ score += 0.1;
38
+ reasons.push("touches the issue's referenced files (+0.10)");
39
+ } else if (input.matchesIssueArea === false) {
40
+ score -= 0.1;
41
+ reasons.push("does not touch the issue's referenced files (-0.10)");
42
+ } else {
43
+ reasons.push("issue-area match unknown (0)");
44
+ }
45
+ return { score: Math.round(clamp01(score) * 100) / 100, reasons };
46
+ }
47
+ export {
48
+ scoreDiffAcceptance
49
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminalhire",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
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",
@@ -26,6 +26,7 @@
26
26
  "README.md"
27
27
  ],
28
28
  "scripts": {
29
+ "test": "for f in test/*.test.js; do echo \"# $f\"; node \"$f\" || exit 1; done",
29
30
  "build": "tsup",
30
31
  "bundle:plugin": "npm run build && rm -rf ../../plugins/terminalhire/dist && cp -R dist ../../plugins/terminalhire/dist && cp package.json ../../plugins/terminalhire/dist/package.json && cp install.js ../../plugins/terminalhire/dist/install.js && cp postinstall.js ../../plugins/terminalhire/dist/postinstall.js",
31
32
  "prepublishOnly": "npm run build",