terminalhire 0.4.4 → 0.4.5

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.
@@ -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) {
@@ -3694,7 +3694,7 @@ ${p.body ?? ""}`)).length;
3694
3694
  return null;
3695
3695
  }
3696
3696
  }
3697
- async function fetchIssueState2(repoFullName, issueNumber) {
3697
+ async function fetchIssue(repoFullName, issueNumber) {
3698
3698
  try {
3699
3699
  const res = await fetch(`${GH_API}/repos/${repoFullName}/issues/${issueNumber}`, {
3700
3700
  headers: GH_HEADERS,
@@ -3702,7 +3702,8 @@ async function fetchIssueState2(repoFullName, issueNumber) {
3702
3702
  });
3703
3703
  if (!res.ok) return null;
3704
3704
  const issue = await res.json();
3705
- return issue.state === "open" ? "open" : issue.state === "closed" ? "closed" : null;
3705
+ const state = issue.state === "open" ? "open" : issue.state === "closed" ? "closed" : null;
3706
+ return { state, title: typeof issue.title === "string" ? issue.title : null };
3706
3707
  } catch {
3707
3708
  return null;
3708
3709
  }
@@ -3730,14 +3731,7 @@ function printMetric(rate) {
3730
3731
  console.log(`
3731
3732
  \u{1F4CA} Accepted-PR rate: ${rate.merged}/${rate.total} claims merged (${pct}%)`);
3732
3733
  }
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
- }
3734
+ async function resolveBounty(arg) {
3741
3735
  let bountyId, title, repoFullName, issueUrl, amountUSD;
3742
3736
  const job = findBountyInCache(arg);
3743
3737
  if (job) {
@@ -3749,11 +3743,7 @@ async function cmdRecord(arg) {
3749
3743
  amountUSD = b.amountUSD ?? null;
3750
3744
  } else {
3751
3745
  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
- }
3746
+ if (!parsed) return null;
3757
3747
  bountyId = `gh:${parsed.repoFullName}#${parsed.number}`;
3758
3748
  title = `${parsed.repoFullName}#${parsed.number}`;
3759
3749
  repoFullName = parsed.repoFullName;
@@ -3761,14 +3751,40 @@ async function cmdRecord(arg) {
3761
3751
  amountUSD = null;
3762
3752
  }
3763
3753
  const ghIssue = parseGitHubUrl(issueUrl);
3764
- const [issueState, openPRs] = ghIssue ? await Promise.all([
3765
- fetchIssueState2(repoFullName, ghIssue.number),
3754
+ const [issue, openPRs] = ghIssue ? await Promise.all([
3755
+ fetchIssue(repoFullName, ghIssue.number),
3766
3756
  countOpenPRsReferencingIssue(repoFullName, ghIssue.number)
3767
- // Guardrail #5
3768
3757
  ]) : [null, null];
3769
- if (issueState === "closed") {
3758
+ const issueState = issue ? issue.state : null;
3759
+ if (!job && issue && issue.title) title = issue.title;
3760
+ return {
3761
+ bountyId,
3762
+ title,
3763
+ repoFullName,
3764
+ issueUrl,
3765
+ amountUSD,
3766
+ issueState,
3767
+ openPRs,
3768
+ issueNumber: ghIssue ? ghIssue.number : null
3769
+ };
3770
+ }
3771
+ async function cmdRecord(arg) {
3772
+ const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
3773
+ if (!arg) {
3774
+ console.error("Usage: terminalhire claim record <bountyId|issueUrl>");
3775
+ console.error(" Run `terminalhire bounties` first to populate the local index cache,");
3776
+ console.error(" then pass the id shown in its output \u2014 or pass a GitHub issue URL directly.");
3777
+ process.exit(1);
3778
+ }
3779
+ const b = await resolveBounty(arg);
3780
+ if (!b) {
3781
+ console.error(`terminalhire claim: '${arg}' is not in the index cache and is not a GitHub issue URL.`);
3782
+ console.error(" Run `terminalhire bounties` to populate the cache, or pass a full issue URL.");
3783
+ process.exit(1);
3784
+ }
3785
+ if (b.issueState === "closed") {
3770
3786
  console.error(
3771
- `terminalhire claim: ${repoFullName}#${ghIssue.number} is CLOSED \u2014 not claimable.
3787
+ `terminalhire claim: ${b.repoFullName}#${b.issueNumber} is CLOSED \u2014 not claimable.
3772
3788
  The bounty index drops closed issues; this one is likely a stale cache entry.
3773
3789
  Run \`terminalhire bounties\` for the current open pool.`
3774
3790
  );
@@ -3776,7 +3792,7 @@ async function cmdRecord(arg) {
3776
3792
  }
3777
3793
  let claim;
3778
3794
  try {
3779
- claim = claims.recordClaim({ id: bountyId, bountyId, title, repoFullName, issueUrl, amountUSD, openPRsAtClaim: openPRs });
3795
+ 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
3796
  } catch (err) {
3781
3797
  console.error(`terminalhire claim: ${err.message ?? err}`);
3782
3798
  process.exit(1);
@@ -3787,10 +3803,10 @@ async function cmdRecord(arg) {
3787
3803
  console.log(` repo: ${claim.repoFullName}`);
3788
3804
  console.log(` amount: ${fmtAmount(claim.amountUSD)}`);
3789
3805
  console.log(` issue: ${claim.issueUrl}`);
3790
- if (openPRs == null) {
3806
+ if (b.openPRs == null) {
3791
3807
  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.`);
3808
+ } else if (b.openPRs > 0) {
3809
+ console.log(` \u26A0 open PRs referencing this issue: ${b.openPRs} \u2014 someone may already be on it. Check before working.`);
3794
3810
  } else {
3795
3811
  console.log(" open PRs referencing this issue: 0");
3796
3812
  }
@@ -3801,6 +3817,49 @@ async function cmdRecord(arg) {
3801
3817
  console.log(" \u2022 no access to ~/.terminalhire (the executor never needs your profile)");
3802
3818
  console.log("\n Next: do the work, then `terminalhire claim update " + claim.id + " <state>` as you progress.");
3803
3819
  }
3820
+ async function cmdPreview(arg, { json } = {}) {
3821
+ if (!arg) {
3822
+ console.error("Usage: terminalhire claim preview <bountyId|issueUrl> [--json]");
3823
+ process.exit(1);
3824
+ }
3825
+ const b = await resolveBounty(arg);
3826
+ if (!b) {
3827
+ console.error(`terminalhire claim: '${arg}' is not in the index cache and is not a GitHub issue URL.`);
3828
+ console.error(" Run `terminalhire bounties` to populate the cache, or pass a full issue URL.");
3829
+ process.exit(1);
3830
+ }
3831
+ if (json) {
3832
+ process.stdout.write(
3833
+ JSON.stringify({
3834
+ bountyId: b.bountyId,
3835
+ title: b.title,
3836
+ amountUSD: b.amountUSD,
3837
+ repoFullName: b.repoFullName,
3838
+ issueUrl: b.issueUrl,
3839
+ issueState: b.issueState,
3840
+ openPRs: b.openPRs
3841
+ }) + "\n"
3842
+ );
3843
+ return;
3844
+ }
3845
+ console.log(`
3846
+ BOUNTY \xB7 ${b.title}`);
3847
+ console.log(` id: ${b.bountyId}`);
3848
+ console.log(` repo: ${b.repoFullName}`);
3849
+ console.log(` amount: ${fmtAmount(b.amountUSD)}`);
3850
+ console.log(` issue: ${b.issueUrl}`);
3851
+ if (b.issueState === "closed") {
3852
+ console.log(" \u2717 CLOSED \u2014 not claimable (the pool drops closed issues; likely a stale cache entry)");
3853
+ }
3854
+ if (b.openPRs == null) {
3855
+ console.log(" open PRs: unknown (GitHub read unavailable \u2014 check the issue manually before working)");
3856
+ } else if (b.openPRs > 0) {
3857
+ console.log(` \u26A0 open PRs referencing this issue: ${b.openPRs} \u2014 someone may already be on it. Check before working.`);
3858
+ } else {
3859
+ console.log(" open PRs referencing this issue: 0");
3860
+ }
3861
+ console.log("\n Preview only \u2014 NOT claimed. Run `terminalhire claim record " + arg + "` to claim it.");
3862
+ }
3804
3863
  async function cmdList(active) {
3805
3864
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
3806
3865
  const list = claims.listClaims({ active });
@@ -3886,8 +3945,12 @@ async function run4() {
3886
3945
  const verb = process.argv[2];
3887
3946
  const rest = process.argv.slice(3).filter((a) => !a.startsWith("--"));
3888
3947
  const active = process.argv.includes("--active");
3948
+ const json = process.argv.includes("--json");
3889
3949
  try {
3890
3950
  switch (verb) {
3951
+ case "preview":
3952
+ await cmdPreview(rest[0], { json });
3953
+ break;
3891
3954
  case "record":
3892
3955
  await cmdRecord(rest[0]);
3893
3956
  break;
@@ -3904,7 +3967,7 @@ async function run4() {
3904
3967
  await cmdRelease(rest[0]);
3905
3968
  break;
3906
3969
  default:
3907
- console.error(`terminalhire claim: unknown verb '${verb ?? ""}'. Expected: record | list | status | update | release`);
3970
+ console.error(`terminalhire claim: unknown verb '${verb ?? ""}'. Expected: preview | record | list | status | update | release`);
3908
3971
  process.exit(1);
3909
3972
  }
3910
3973
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminalhire",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
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",