terminalhire 0.4.6 → 0.6.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.
package/README.md CHANGED
@@ -45,6 +45,17 @@ terminalhire jobs --limit 20 # show top 20 results (default: 10)
45
45
  terminalhire jobs --remote-only # filter to remote roles only
46
46
  terminalhire jobs --all # show all matches above zero score
47
47
 
48
+ terminalhire bounties # day-sized paid tasks you can knock out today
49
+ terminalhire bounties --priced # only bounties with a known $ amount
50
+
51
+ terminalhire claim record <id|issueUrl> # claim a bounty locally + print the executor brief
52
+ terminalhire claim list --active # list your active claims + accepted-PR rate
53
+ terminalhire claim status <id> # poll source PR merge state (updates the metric)
54
+
55
+ terminalhire trajectory # trajectory from your local Claude Code corpus
56
+ terminalhire trajectory --export # write a derived score + Markdown to ~/.terminalhire/
57
+ terminalhire trajectory --inward # also show private rework/recovery (never exported)
58
+
48
59
  terminalhire profile --show # inspect your encrypted local profile (incl. GitHub fields)
49
60
  terminalhire profile --edit # set displayName, contactEmail, prefs
50
61
  terminalhire profile --delete # wipe profile and encryption key from disk
@@ -680,6 +680,52 @@ function githubToFingerprint(p) {
680
680
  const seniorityBand = inferSeniority(p);
681
681
  return { skillTags, seniorityBand };
682
682
  }
683
+ async function fetchOwnedRepoTraction(login, token) {
684
+ const computedAt = (/* @__PURE__ */ new Date()).toISOString();
685
+ const empty = (status) => ({
686
+ status,
687
+ totalStars: 0,
688
+ totalForks: 0,
689
+ reposWithStars: 0,
690
+ top: [],
691
+ computedAt
692
+ });
693
+ let repos;
694
+ try {
695
+ repos = await ghFetch(
696
+ `/users/${login}/repos?sort=pushed&per_page=100`,
697
+ token
698
+ );
699
+ } catch (err) {
700
+ const msg = err instanceof Error ? err.message : String(err);
701
+ const status = /HTTP 403|HTTP 429|rate limit/i.test(msg) ? "rate-limited" : "failed";
702
+ console.warn(`[github] ${login}: traction fetch failed (${status}) \u2014`, msg);
703
+ return empty(status);
704
+ }
705
+ if (!Array.isArray(repos)) return empty("failed");
706
+ const owned = repos.filter((r) => r && !r.fork && !r.archived);
707
+ let totalStars = 0;
708
+ let totalForks = 0;
709
+ let reposWithStars = 0;
710
+ const ranked = [];
711
+ for (const r of owned) {
712
+ const stars = typeof r.stargazers_count === "number" ? r.stargazers_count : 0;
713
+ const forks = typeof r.forks_count === "number" ? r.forks_count : 0;
714
+ totalStars += stars;
715
+ totalForks += forks;
716
+ if (stars >= 1) reposWithStars++;
717
+ ranked.push({ name: r.name, stars, forks });
718
+ }
719
+ ranked.sort((a, b) => b.stars - a.stars || b.forks - a.forks);
720
+ return {
721
+ status: "ok",
722
+ totalStars,
723
+ totalForks,
724
+ reposWithStars,
725
+ top: ranked.slice(0, TRACTION_TOP_N),
726
+ computedAt
727
+ };
728
+ }
683
729
  async function ghFetchRaw(path, token) {
684
730
  return fetch(`https://api.github.com${path}`, { headers: ghHeaders(token) });
685
731
  }
@@ -888,11 +934,12 @@ function deriveResumeTrend(cred, repoRecency, now = Date.now()) {
888
934
  }
889
935
  return scored.sort((a, b) => b.weight - a.weight).slice(0, 12).map((s) => s.t);
890
936
  }
891
- var MIN_STARS, MIN_CONTRIBUTORS, CANDIDATE_PR_PAGE, TRIVIAL_PR_TITLE, RESUME_DECAY_HALF_LIFE_MS, RESUME_MIN_SCORE;
937
+ var TRACTION_TOP_N, MIN_STARS, MIN_CONTRIBUTORS, CANDIDATE_PR_PAGE, TRIVIAL_PR_TITLE, RESUME_DECAY_HALF_LIFE_MS, RESUME_MIN_SCORE;
892
938
  var init_github = __esm({
893
939
  "../../packages/core/src/github.ts"() {
894
940
  "use strict";
895
941
  init_vocabulary();
942
+ TRACTION_TOP_N = 6;
896
943
  MIN_STARS = 50;
897
944
  MIN_CONTRIBUTORS = 10;
898
945
  CANDIDATE_PR_PAGE = 50;
@@ -2571,6 +2618,7 @@ __export(src_exports, {
2571
2618
  expandWeighted: () => expandWeighted,
2572
2619
  extractSkillTags: () => extractSkillTags,
2573
2620
  fetchGitHubProfile: () => fetchGitHubProfile,
2621
+ fetchOwnedRepoTraction: () => fetchOwnedRepoTraction,
2574
2622
  fetchRepoRecency: () => fetchRepoRecency,
2575
2623
  flattenTiers: () => flattenTiers,
2576
2624
  getBuyer: () => getBuyer,
@@ -106,10 +106,56 @@ var init_claims = __esm({
106
106
  import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
107
107
  import { join as join2 } from "path";
108
108
  import { homedir as homedir2 } from "os";
109
+ import { execFile } from "child_process";
110
+ import { promisify } from "util";
111
+ import { createInterface } from "readline";
109
112
  var TERMINALHIRE_DIR2 = join2(homedir2(), ".terminalhire");
110
113
  var INDEX_CACHE_FILE = join2(TERMINALHIRE_DIR2, "index-cache.json");
111
114
  var GH_API = "https://api.github.com";
112
115
  var GH_HEADERS = { "User-Agent": "terminalhire-claim", Accept: "application/vnd.github+json" };
116
+ var pExecFile = promisify(execFile);
117
+ async function sh(cmd, args, opts = {}) {
118
+ const { stdout } = await pExecFile(cmd, args, { ...opts, shell: false, maxBuffer: 16 * 1024 * 1024 });
119
+ return String(stdout).trim();
120
+ }
121
+ async function confirm(question) {
122
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
123
+ try {
124
+ const ans = await new Promise((resolve) => rl.question(question, resolve));
125
+ return /^y(es)?$/i.test(String(ans).trim());
126
+ } finally {
127
+ rl.close();
128
+ }
129
+ }
130
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["worktree", "branch"]);
131
+ function parseArgs(argv) {
132
+ const flags = {};
133
+ const positional = [];
134
+ for (let i = 0; i < argv.length; i++) {
135
+ const a = argv[i];
136
+ if (a.startsWith("--")) {
137
+ const key = a.slice(2);
138
+ if (VALUE_FLAGS.has(key)) {
139
+ const val = argv[i + 1];
140
+ if (val === void 0 || val.startsWith("--")) {
141
+ console.error(`terminalhire claim: --${key} requires a value.`);
142
+ process.exit(1);
143
+ }
144
+ flags[key] = val;
145
+ i++;
146
+ } else {
147
+ flags[key] = true;
148
+ }
149
+ } else {
150
+ positional.push(a);
151
+ }
152
+ }
153
+ return { flags, positional };
154
+ }
155
+ function parseRepoFromRemote(url) {
156
+ const m = String(url ?? "").trim().match(/github\.com[:/]+([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
157
+ return m ? `${m[1]}/${m[2]}` : null;
158
+ }
113
159
  function findBountyInCache(bountyId) {
114
160
  try {
115
161
  if (!existsSync2(INDEX_CACHE_FILE)) return null;
@@ -264,7 +310,10 @@ async function cmdRecord(arg) {
264
310
  console.log(" \u2022 MUST NOT `git push` or `gh pr` \u2014 pushing happens only via `terminalhire submit`");
265
311
  console.log(" \u2022 clone + static analysis + patch only; NO test/build execution without explicit approval");
266
312
  console.log(" \u2022 no access to ~/.terminalhire (the executor never needs your profile)");
267
- console.log("\n Next: do the work, then `terminalhire claim update " + claim.id + " <state>` as you progress.");
313
+ console.log("\n Next:");
314
+ console.log(" 1. record the worktree: terminalhire claim attach " + claim.id + " --worktree <path> --branch <branch>");
315
+ console.log(" 2. do the work + review, then mark it cleared: terminalhire claim update " + claim.id + " ready");
316
+ console.log(" 3. publish (pushes to your fork + opens the PR): terminalhire claim submit " + claim.id);
268
317
  }
269
318
  async function cmdPreview(arg, { json } = {}) {
270
319
  if (!arg) {
@@ -390,33 +439,223 @@ async function cmdRelease(id) {
390
439
  console.log(removed ? `Released claim: ${id}` : `terminalhire claim: no claim with id '${id}'.`);
391
440
  if (!removed) process.exit(1);
392
441
  }
442
+ async function cmdAttach(id, worktree, branch) {
443
+ const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
444
+ if (!id || !worktree || !branch) {
445
+ console.error("Usage: terminalhire claim attach <id> --worktree <path> --branch <branchName>");
446
+ console.error(" Records the worktree + branch so `terminalhire claim submit` can verify identity before pushing.");
447
+ process.exit(1);
448
+ }
449
+ if (!claims.findClaim(id)) {
450
+ console.error(`terminalhire claim: no claim with id '${id}'.`);
451
+ process.exit(1);
452
+ }
453
+ let toplevel;
454
+ try {
455
+ toplevel = await sh("git", ["-C", worktree, "rev-parse", "--show-toplevel"]);
456
+ } catch {
457
+ console.error(`terminalhire claim: '${worktree}' is not a git work tree.`);
458
+ process.exit(1);
459
+ }
460
+ claims.updateClaim(id, { worktreePath: toplevel, branch });
461
+ console.log(`Attached ${id}: worktree=${toplevel} branch=${branch}`);
462
+ }
463
+ async function cmdSubmit(id, worktreeOverride) {
464
+ const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
465
+ if (!id) {
466
+ console.error("Usage: terminalhire claim submit <id> [--worktree <path>]");
467
+ process.exit(1);
468
+ }
469
+ const claim = claims.findClaim(id);
470
+ if (!claim) {
471
+ console.error(`terminalhire claim: no claim with id '${id}'.`);
472
+ process.exit(1);
473
+ }
474
+ if (claim.state !== "ready") {
475
+ console.error(
476
+ `terminalhire claim: ${id} is '${claim.state}', not 'ready'. Submit only runs after the review gate clears it:
477
+ terminalhire claim update ${id} ready`
478
+ );
479
+ process.exit(1);
480
+ }
481
+ if (claim.review && claim.review.verdict === "revise") {
482
+ console.error(`terminalhire claim: ${id} review verdict is 'revise' \u2014 the gate said do not submit. Resolve blockers and re-review first.`);
483
+ process.exit(1);
484
+ }
485
+ if (!claim.worktreePath || !claim.branch) {
486
+ console.error(
487
+ `terminalhire claim: ${id} has no recorded worktree/branch \u2014 cannot verify what to push. Run:
488
+ terminalhire claim attach ${id} --worktree <path> --branch <branch>`
489
+ );
490
+ process.exit(1);
491
+ }
492
+ const inspectDir = worktreeOverride || process.cwd();
493
+ let toplevel;
494
+ try {
495
+ toplevel = await sh("git", ["-C", inspectDir, "rev-parse", "--show-toplevel"]);
496
+ } catch {
497
+ console.error(
498
+ `terminalhire claim: '${inspectDir}' is not a git work tree.
499
+ Run submit from inside the claim's worktree (or pass --worktree <path>).`
500
+ );
501
+ process.exit(1);
502
+ }
503
+ if (toplevel !== claim.worktreePath) {
504
+ console.error(
505
+ `terminalhire claim: worktree mismatch \u2014 refusing to push.
506
+ expected: ${claim.worktreePath}
507
+ found: ${toplevel}
508
+ Run submit from inside the claim's worktree (or pass --worktree <path>).`
509
+ );
510
+ process.exit(1);
511
+ }
512
+ const wt = toplevel;
513
+ const curBranch = await sh("git", ["-C", wt, "rev-parse", "--abbrev-ref", "HEAD"]);
514
+ if (curBranch !== claim.branch) {
515
+ console.error(
516
+ `terminalhire claim: branch mismatch \u2014 refusing to push.
517
+ expected: ${claim.branch}
518
+ found: ${curBranch}
519
+ Check out the claim's branch first.`
520
+ );
521
+ process.exit(1);
522
+ }
523
+ let defaultBranch = null;
524
+ try {
525
+ defaultBranch = (await sh("git", ["-C", wt, "symbolic-ref", "--short", "refs/remotes/origin/HEAD"])).replace(/^origin\//, "");
526
+ } catch {
527
+ }
528
+ if (defaultBranch && claim.branch === defaultBranch) {
529
+ console.error(`terminalhire claim: '${claim.branch}' is the default branch \u2014 open the PR from a feature branch.`);
530
+ process.exit(1);
531
+ }
532
+ if (defaultBranch) {
533
+ let ahead = "0";
534
+ try {
535
+ ahead = await sh("git", ["-C", wt, "rev-list", "--count", `origin/${defaultBranch}..HEAD`]);
536
+ } catch {
537
+ ahead = "1";
538
+ }
539
+ if (ahead === "0") {
540
+ console.error(`terminalhire claim: branch has no commits ahead of origin/${defaultBranch} \u2014 nothing to submit.`);
541
+ process.exit(1);
542
+ }
543
+ }
544
+ const dirty = await sh("git", ["-C", wt, "status", "--porcelain"]);
545
+ if (dirty) {
546
+ console.error("terminalhire claim: working tree is not clean \u2014 commit or stash before submitting (submit pushes what was reviewed).");
547
+ process.exit(1);
548
+ }
549
+ let originUrl;
550
+ try {
551
+ originUrl = await sh("git", ["-C", wt, "remote", "get-url", "origin"]);
552
+ } catch {
553
+ console.error("terminalhire claim: no 'origin' remote in the worktree.");
554
+ process.exit(1);
555
+ }
556
+ const originRepo = parseRepoFromRemote(originUrl);
557
+ if (!originRepo) {
558
+ console.error(`terminalhire claim: could not parse owner/repo from origin (${originUrl}).`);
559
+ process.exit(1);
560
+ }
561
+ if (originRepo.toLowerCase() === claim.repoFullName.toLowerCase()) {
562
+ console.error(
563
+ `terminalhire claim: origin points at the UPSTREAM bounty repo (${claim.repoFullName}), not a fork.
564
+ Pushing would create a branch directly on the target repo. Fork first:
565
+ gh repo fork ${claim.repoFullName} --clone=false
566
+ set your fork as 'origin' (or push it there), then retry.`
567
+ );
568
+ process.exit(1);
569
+ }
570
+ let ghUser;
571
+ try {
572
+ ghUser = await sh("gh", ["api", "user", "-q", ".login"]);
573
+ } catch {
574
+ console.error("terminalhire claim: 'gh' CLI not available or not authenticated. Run 'gh auth login'.");
575
+ process.exit(1);
576
+ }
577
+ const upstream = claim.repoFullName;
578
+ const head = `${ghUser}:${claim.branch}`;
579
+ console.log(`
580
+ SUBMIT \xB7 ${claim.title}`);
581
+ console.log(` upstream: ${upstream}`);
582
+ console.log(` fork: ${originRepo}`);
583
+ console.log(` branch: ${claim.branch}`);
584
+ console.log(` head: ${head}`);
585
+ console.log(` issue: ${claim.issueUrl}`);
586
+ const ok = await confirm(`
587
+ Push '${claim.branch}' to ${originRepo} and open a PR against ${upstream}? (y/N) `);
588
+ if (!ok) {
589
+ console.log("Aborted \u2014 nothing pushed.");
590
+ return;
591
+ }
592
+ try {
593
+ await sh("git", ["-C", wt, "push", "origin", claim.branch]);
594
+ } catch (err) {
595
+ console.error(`terminalhire claim: git push failed (NOT force-pushed). ${err.stderr || err.message || err}`);
596
+ console.error(` Resolve and retry, or open the PR manually then: terminalhire claim update ${id} submitted <prUrl>`);
597
+ process.exit(1);
598
+ }
599
+ let prUrl = null;
600
+ try {
601
+ const existing = await sh("gh", ["pr", "list", "--repo", upstream, "--head", head, "--state", "open", "--json", "url", "-q", ".[0].url // empty"]);
602
+ if (existing) prUrl = existing;
603
+ } catch {
604
+ }
605
+ if (!prUrl) {
606
+ const issueNum = (parseGitHubUrl(claim.issueUrl) || {}).number;
607
+ const body = issueNum ? `Closes #${issueNum}` : "";
608
+ try {
609
+ const out = await sh("gh", ["pr", "create", "--repo", upstream, "--head", head, "--title", claim.title, "--body", body]);
610
+ prUrl = out.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("https://github.com/")).pop() || null;
611
+ } catch (err) {
612
+ console.error(`terminalhire claim: branch pushed, but 'gh pr create' failed. ${err.stderr || err.message || err}`);
613
+ console.error(` Open the PR manually (gh pr create / web UI), then: terminalhire claim update ${id} submitted <prUrl>`);
614
+ process.exit(1);
615
+ }
616
+ }
617
+ if (!prUrl || !parseGitHubUrl(prUrl)) {
618
+ console.error(`terminalhire claim: could not determine the PR URL. Set it manually: terminalhire claim update ${id} submitted <prUrl>`);
619
+ process.exit(1);
620
+ }
621
+ claims.updateClaim(id, { state: "submitted", prUrl });
622
+ console.log(`
623
+ \u2713 Submitted ${id} \u2192 ${prUrl}`);
624
+ console.log(` Run 'terminalhire claim status ${id}' after the maintainer acts to fold the merge into your accepted-PR rate.`);
625
+ }
393
626
  async function run() {
394
627
  const verb = process.argv[2];
395
- const rest = process.argv.slice(3).filter((a) => !a.startsWith("--"));
396
- const active = process.argv.includes("--active");
397
- const json = process.argv.includes("--json");
628
+ const { flags, positional } = parseArgs(process.argv.slice(3));
629
+ const active = Boolean(flags.active);
630
+ const json = Boolean(flags.json);
398
631
  try {
399
632
  switch (verb) {
400
633
  case "preview":
401
- await cmdPreview(rest[0], { json });
634
+ await cmdPreview(positional[0], { json });
402
635
  break;
403
636
  case "record":
404
- await cmdRecord(rest[0]);
637
+ await cmdRecord(positional[0]);
405
638
  break;
406
639
  case "list":
407
640
  await cmdList(active);
408
641
  break;
409
642
  case "status":
410
- await cmdStatus(rest[0]);
643
+ await cmdStatus(positional[0]);
411
644
  break;
412
645
  case "update":
413
- await cmdUpdate(rest[0], rest[1], rest[2]);
646
+ await cmdUpdate(positional[0], positional[1], positional[2]);
647
+ break;
648
+ case "attach":
649
+ await cmdAttach(positional[0], flags.worktree, flags.branch);
650
+ break;
651
+ case "submit":
652
+ await cmdSubmit(positional[0], flags.worktree);
414
653
  break;
415
654
  case "release":
416
- await cmdRelease(rest[0]);
655
+ await cmdRelease(positional[0]);
417
656
  break;
418
657
  default:
419
- console.error(`terminalhire claim: unknown verb '${verb ?? ""}'. Expected: preview | record | list | status | update | release`);
658
+ console.error(`terminalhire claim: unknown verb '${verb ?? ""}'. Expected: preview | record | attach | list | status | update | submit | release`);
420
659
  process.exit(1);
421
660
  }
422
661
  } catch (err) {