terminalhire 0.6.0 → 0.7.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.
@@ -17,19 +17,19 @@ __export(open_url_exports, {
17
17
  import { spawn } from "child_process";
18
18
  function openInBrowser(url) {
19
19
  let cmd;
20
- let args3;
20
+ let args5;
21
21
  if (process.platform === "darwin") {
22
22
  cmd = "open";
23
- args3 = [url];
23
+ args5 = [url];
24
24
  } else if (process.platform === "win32") {
25
25
  cmd = "cmd";
26
- args3 = ["/c", "start", "", url];
26
+ args5 = ["/c", "start", "", url];
27
27
  } else {
28
28
  cmd = "xdg-open";
29
- args3 = [url];
29
+ args5 = [url];
30
30
  }
31
31
  try {
32
- const child = spawn(cmd, args3, { stdio: "ignore", detached: true });
32
+ const child = spawn(cmd, args5, { stdio: "ignore", detached: true });
33
33
  child.on("error", () => {
34
34
  });
35
35
  child.unref();
@@ -1940,14 +1940,14 @@ async function mapWithConcurrency(items, limit, fn) {
1940
1940
  if (items.length === 0) return results;
1941
1941
  const workers = Math.max(1, Math.min(Math.floor(limit) || 1, items.length));
1942
1942
  let next = 0;
1943
- async function run14() {
1943
+ async function run17() {
1944
1944
  for (; ; ) {
1945
1945
  const i = next++;
1946
1946
  if (i >= items.length) return;
1947
1947
  results[i] = await fn(items[i], i);
1948
1948
  }
1949
1949
  }
1950
- await Promise.all(Array.from({ length: workers }, run14));
1950
+ await Promise.all(Array.from({ length: workers }, run17));
1951
1951
  return results;
1952
1952
  }
1953
1953
  var init_concurrency = __esm({
@@ -2515,6 +2515,51 @@ var init_workable = __esm({
2515
2515
  }
2516
2516
  });
2517
2517
 
2518
+ // ../../packages/core/src/feeds/directory.ts
2519
+ function personCardToJob(row) {
2520
+ const tags = [...row.skill_tags];
2521
+ return {
2522
+ id: `dev:${row.login}`,
2523
+ source: "person",
2524
+ title: row.name ?? row.login,
2525
+ company: row.login,
2526
+ url: `/r/${row.login}`,
2527
+ remote: true,
2528
+ tags,
2529
+ coreTags: tags.slice(0, TOP_CORE_TAGS),
2530
+ roleType: "full_time",
2531
+ applyMode: "direct"
2532
+ };
2533
+ }
2534
+ function buildDirectoryIndex(people, opts) {
2535
+ return {
2536
+ builtAt: opts?.builtAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2537
+ cards: people.map(personCardToJob)
2538
+ };
2539
+ }
2540
+ function projectCardToJob(row) {
2541
+ const tags = [...row.needed_skills];
2542
+ return {
2543
+ id: `proj:${row.id}`,
2544
+ source: "project",
2545
+ title: row.title,
2546
+ company: row.owner_login,
2547
+ url: `/r/${row.owner_login}`,
2548
+ remote: true,
2549
+ tags,
2550
+ coreTags: tags.slice(0, TOP_CORE_TAGS),
2551
+ roleType: "full_time",
2552
+ applyMode: "direct"
2553
+ };
2554
+ }
2555
+ var TOP_CORE_TAGS;
2556
+ var init_directory = __esm({
2557
+ "../../packages/core/src/feeds/directory.ts"() {
2558
+ "use strict";
2559
+ TOP_CORE_TAGS = 4;
2560
+ }
2561
+ });
2562
+
2518
2563
  // ../../packages/core/src/feeds/index.ts
2519
2564
  async function aggregateBounties(opts) {
2520
2565
  const [gh, op] = await Promise.all([
@@ -2631,6 +2676,7 @@ var init_feeds = __esm({
2631
2676
  init_github_bounties();
2632
2677
  init_opire();
2633
2678
  init_workable();
2679
+ init_directory();
2634
2680
  init_bounty_gate();
2635
2681
  init_bounty_gate();
2636
2682
  FEEDS = [greenhouse, ashby, lever, workable, himalayas, wwr, hn];
@@ -2831,6 +2877,176 @@ var init_indexer = __esm({
2831
2877
  }
2832
2878
  });
2833
2879
 
2880
+ // ../../packages/core/src/intro.ts
2881
+ function buildIntroPayload(input) {
2882
+ const payload = {
2883
+ requesterLogin: input.requesterLogin,
2884
+ requesterDisplayName: input.requesterDisplayName,
2885
+ requesterContact: input.requesterContact,
2886
+ targetLogin: input.targetLogin
2887
+ };
2888
+ const note = input.note?.trim();
2889
+ if (note) payload.note = note;
2890
+ return payload;
2891
+ }
2892
+ function rejectExtraIntroFields(body) {
2893
+ for (const key of Object.keys(body)) {
2894
+ if (!INTRO_ALLOWED_SET.has(key)) {
2895
+ return `intro payload contains disallowed field: "${key}"`;
2896
+ }
2897
+ }
2898
+ return null;
2899
+ }
2900
+ function validateIntroPayload(body) {
2901
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
2902
+ return { ok: false, reason: "intro payload must be a JSON object" };
2903
+ }
2904
+ const b = body;
2905
+ const extra = rejectExtraIntroFields(b);
2906
+ if (extra) return { ok: false, reason: extra };
2907
+ const ok = (v, max) => typeof v === "string" && v.trim().length > 0 && v.length <= max;
2908
+ if (!ok(b.requesterLogin, MAX_SHORT)) return { ok: false, reason: "requesterLogin is required" };
2909
+ if (!ok(b.requesterDisplayName, MAX_SHORT)) return { ok: false, reason: "requesterDisplayName is required" };
2910
+ if (!ok(b.requesterContact, MAX_SHORT)) return { ok: false, reason: "requesterContact is required" };
2911
+ if (!ok(b.targetLogin, MAX_SHORT)) return { ok: false, reason: "targetLogin is required" };
2912
+ if (b.note !== void 0 && (typeof b.note !== "string" || b.note.length > MAX_NOTE)) {
2913
+ return { ok: false, reason: "note must be a string of at most 500 chars" };
2914
+ }
2915
+ const value = {
2916
+ requesterLogin: b.requesterLogin.trim(),
2917
+ requesterDisplayName: b.requesterDisplayName.trim(),
2918
+ requesterContact: b.requesterContact.trim(),
2919
+ targetLogin: b.targetLogin.trim()
2920
+ };
2921
+ const note = typeof b.note === "string" ? b.note.trim() : "";
2922
+ if (note) value.note = note;
2923
+ return { ok: true, value };
2924
+ }
2925
+ function introRateLimitCheck(history, now, opts) {
2926
+ const cutoff = now - opts.windowMs;
2927
+ const recent = history.filter((t) => t > cutoff);
2928
+ if (recent.length >= opts.max) {
2929
+ const oldest = recent[0] ?? now;
2930
+ return { allowed: false, retained: recent, retryAfterMs: Math.max(0, oldest + opts.windowMs - now) };
2931
+ }
2932
+ return { allowed: true, retained: [...recent, now], retryAfterMs: 0 };
2933
+ }
2934
+ function isOverIntroLimit(recentCount, max) {
2935
+ return recentCount >= max;
2936
+ }
2937
+ function composeIntroEmail(args5) {
2938
+ const subject = `New intro request from @${args5.requesterLogin} \xB7 terminalhire`;
2939
+ const text = `@${args5.requesterLogin} wants an intro to you on terminalhire.
2940
+
2941
+ Sign in to view the request and choose whether to share your contact back:
2942
+ ${args5.dashboardUrl}
2943
+
2944
+ You control whether this connects \u2014 no contact details are shared unless you accept.
2945
+
2946
+ \u2014 Terminalhire`;
2947
+ return { subject, text };
2948
+ }
2949
+ function introActorRole(intro, actorLogin) {
2950
+ const a = actorLogin.trim().toLowerCase();
2951
+ if (a && a === intro.targetLogin.trim().toLowerCase()) return "target";
2952
+ if (a && a === intro.requesterLogin.trim().toLowerCase()) return "requester";
2953
+ return "other";
2954
+ }
2955
+ function authorizeIntroDecision(intro, actorLogin) {
2956
+ const role = introActorRole(intro, actorLogin);
2957
+ if (role === "target") return { ok: true };
2958
+ if (role === "requester") {
2959
+ return { ok: false, status: 403, reason: "the requester cannot accept or decline their own intro request" };
2960
+ }
2961
+ return { ok: false, status: 404, reason: "intro not found" };
2962
+ }
2963
+ function authorizeIntroDeletion(intro, actorLogin) {
2964
+ const role = introActorRole(intro, actorLogin);
2965
+ if (role === "other") return { ok: false, status: 404, reason: "intro not found" };
2966
+ return { ok: true };
2967
+ }
2968
+ function revealIntroContacts(intro) {
2969
+ if (intro.status !== "accepted") return { toRequester: null, toTarget: null };
2970
+ return { toRequester: intro.targetContact ?? null, toTarget: intro.requesterContact };
2971
+ }
2972
+ function validateTargetContact(v) {
2973
+ if (typeof v !== "string" || v.trim().length === 0) return { ok: false, reason: "targetContact is required" };
2974
+ if (v.length > MAX_SHORT) return { ok: false, reason: `targetContact must be at most ${MAX_SHORT} chars` };
2975
+ return { ok: true, value: v.trim() };
2976
+ }
2977
+ function buildIntroListItem(intro, viewerLogin) {
2978
+ const role = introActorRole(intro, viewerLogin);
2979
+ if (role === "other") return null;
2980
+ const reveal = revealIntroContacts(intro);
2981
+ if (role === "target") {
2982
+ return {
2983
+ id: intro.id,
2984
+ role: "incoming",
2985
+ counterpartyLogin: intro.requesterLogin,
2986
+ status: intro.status,
2987
+ note: intro.note ?? null,
2988
+ contact: reveal.toTarget
2989
+ };
2990
+ }
2991
+ return {
2992
+ id: intro.id,
2993
+ role: "outgoing",
2994
+ counterpartyLogin: intro.targetLogin,
2995
+ status: intro.status,
2996
+ note: intro.note ?? null,
2997
+ contact: reveal.toRequester
2998
+ };
2999
+ }
3000
+ function composeIntroAcceptedEmail(args5) {
3001
+ const subject = `Intro connected with @${args5.counterpartyLogin} \xB7 terminalhire`;
3002
+ const lead = args5.recipientRole === "requester" ? `@${args5.counterpartyLogin} accepted your intro request on terminalhire.` : `You accepted @${args5.counterpartyLogin}'s intro request on terminalhire.`;
3003
+ const text = `${lead}
3004
+
3005
+ You can now reach them directly:
3006
+ @${args5.counterpartyLogin} \u2014 ${args5.counterpartyContact}
3007
+
3008
+ Take it from here.
3009
+
3010
+ \u2014 Terminalhire`;
3011
+ return { subject, text };
3012
+ }
3013
+ function introRetentionAction(row, now) {
3014
+ if (row.status === "pending") {
3015
+ const created = Date.parse(row.createdAt);
3016
+ if (Number.isFinite(created) && now - created > INTRO_PENDING_TTL_MS) return "purge";
3017
+ return "keep";
3018
+ }
3019
+ if (row.status === "declined") {
3020
+ return row.hasContact ? "scrub-declined" : "keep";
3021
+ }
3022
+ if (row.status === "accepted") {
3023
+ const updated = Date.parse(row.updatedAt);
3024
+ if (row.hasContact && Number.isFinite(updated) && now - updated > INTRO_ACCEPTED_TTL_MS) {
3025
+ return "expire-accepted";
3026
+ }
3027
+ return "keep";
3028
+ }
3029
+ return "keep";
3030
+ }
3031
+ var INTRO_ALLOWED_FIELDS, INTRO_ALLOWED_SET, MAX_SHORT, MAX_NOTE, INTRO_PENDING_TTL_MS, INTRO_ACCEPTED_TTL_MS;
3032
+ var init_intro = __esm({
3033
+ "../../packages/core/src/intro.ts"() {
3034
+ "use strict";
3035
+ INTRO_ALLOWED_FIELDS = [
3036
+ "requesterLogin",
3037
+ "requesterDisplayName",
3038
+ "requesterContact",
3039
+ "note",
3040
+ "targetLogin"
3041
+ ];
3042
+ INTRO_ALLOWED_SET = new Set(INTRO_ALLOWED_FIELDS);
3043
+ MAX_SHORT = 200;
3044
+ MAX_NOTE = 500;
3045
+ INTRO_PENDING_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
3046
+ INTRO_ACCEPTED_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
3047
+ }
3048
+ });
3049
+
2834
3050
  // ../../packages/core/src/index.ts
2835
3051
  var src_exports = {};
2836
3052
  __export(src_exports, {
@@ -2846,6 +3062,9 @@ __export(src_exports, {
2846
3062
  GRAPH: () => GRAPH,
2847
3063
  GREENHOUSE_SLUGS_BY_TIER: () => GREENHOUSE_SLUGS_BY_TIER,
2848
3064
  IDF_BACKGROUND: () => IDF_BACKGROUND,
3065
+ INTRO_ACCEPTED_TTL_MS: () => INTRO_ACCEPTED_TTL_MS,
3066
+ INTRO_ALLOWED_FIELDS: () => INTRO_ALLOWED_FIELDS,
3067
+ INTRO_PENDING_TTL_MS: () => INTRO_PENDING_TTL_MS,
2849
3068
  LEVER_SLUGS_BY_TIER: () => LEVER_SLUGS_BY_TIER,
2850
3069
  SYNONYMS: () => SYNONYMS,
2851
3070
  VOCABULARY: () => VOCABULARY,
@@ -2854,10 +3073,17 @@ __export(src_exports, {
2854
3073
  aggregate: () => aggregate,
2855
3074
  aggregateBounties: () => aggregateBounties,
2856
3075
  ashby: () => ashby,
3076
+ authorizeIntroDecision: () => authorizeIntroDecision,
3077
+ authorizeIntroDeletion: () => authorizeIntroDeletion,
2857
3078
  bestAcceptanceDomain: () => bestAcceptanceDomain,
3079
+ buildDirectoryIndex: () => buildDirectoryIndex,
2858
3080
  buildGraph: () => buildGraph,
2859
3081
  buildIndex: () => buildIndex,
3082
+ buildIntroListItem: () => buildIntroListItem,
3083
+ buildIntroPayload: () => buildIntroPayload,
2860
3084
  buildReason: () => buildReason,
3085
+ composeIntroAcceptedEmail: () => composeIntroAcceptedEmail,
3086
+ composeIntroEmail: () => composeIntroEmail,
2861
3087
  computeAcceptanceCredential: () => computeAcceptanceCredential,
2862
3088
  computeAcceptanceCredentialPublic: () => computeAcceptanceCredentialPublic,
2863
3089
  coreTagsFromTitle: () => coreTagsFromTitle,
@@ -2874,7 +3100,11 @@ __export(src_exports, {
2874
3100
  greenhouse: () => greenhouse,
2875
3101
  himalayas: () => himalayas,
2876
3102
  hn: () => hn,
3103
+ introActorRole: () => introActorRole,
3104
+ introRateLimitCheck: () => introRateLimitCheck,
3105
+ introRetentionAction: () => introRetentionAction,
2877
3106
  isBounty: () => isBounty,
3107
+ isOverIntroLimit: () => isOverIntroLimit,
2878
3108
  lever: () => lever,
2879
3109
  loadPartnerRoles: () => loadPartnerRoles,
2880
3110
  looksLikeEngRole: () => looksLikeEngRole,
@@ -2882,8 +3112,14 @@ __export(src_exports, {
2882
3112
  normalize: () => normalize,
2883
3113
  opire: () => opire,
2884
3114
  passesMaturityGate: () => passesMaturityGate,
3115
+ personCardToJob: () => personCardToJob,
3116
+ projectCardToJob: () => projectCardToJob,
3117
+ rejectExtraIntroFields: () => rejectExtraIntroFields,
3118
+ revealIntroContacts: () => revealIntroContacts,
2885
3119
  tokenize: () => tokenize,
2886
3120
  validateGraph: () => validateGraph,
3121
+ validateIntroPayload: () => validateIntroPayload,
3122
+ validateTargetContact: () => validateTargetContact,
2887
3123
  workable: () => workable,
2888
3124
  wwr: () => wwr
2889
3125
  });
@@ -2897,6 +3133,7 @@ var init_src = __esm({
2897
3133
  init_indexer();
2898
3134
  init_partners();
2899
3135
  init_github();
3136
+ init_intro();
2900
3137
  }
2901
3138
  });
2902
3139
 
@@ -3174,11 +3411,11 @@ async function runLogin() {
3174
3411
  if (process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["JPI_GITHUB_MOCK"] === "1") {
3175
3412
  const { createRequire: createRequire2 } = await import("module");
3176
3413
  const { fileURLToPath: fileURLToPath7 } = await import("url");
3177
- const { join: join18, dirname: dirname3 } = await import("path");
3414
+ const { join: join20, dirname: dirname3 } = await import("path");
3178
3415
  const __dirname6 = fileURLToPath7(new URL(".", import.meta.url));
3179
- const fixturePath = join18(__dirname6, "../../fixtures/github-sample.json");
3180
- const { readFileSync: readFileSync17 } = await import("fs");
3181
- ghProfile = JSON.parse(readFileSync17(fixturePath, "utf8"));
3416
+ const fixturePath = join20(__dirname6, "../../fixtures/github-sample.json");
3417
+ const { readFileSync: readFileSync19 } = await import("fs");
3418
+ ghProfile = JSON.parse(readFileSync19(fixturePath, "utf8"));
3182
3419
  } else {
3183
3420
  ghProfile = await fetchGitHubProfile2(login, token);
3184
3421
  }
@@ -3543,39 +3780,335 @@ var init_jpi_jobs = __esm({
3543
3780
  }
3544
3781
  });
3545
3782
 
3546
- // bin/jpi-bounties.js
3547
- var jpi_bounties_exports = {};
3548
- __export(jpi_bounties_exports, {
3783
+ // bin/jpi-devs.js
3784
+ var jpi_devs_exports = {};
3785
+ __export(jpi_devs_exports, {
3786
+ reportMatched: () => reportMatched,
3549
3787
  run: () => run3
3550
3788
  });
3551
3789
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
3552
3790
  import { join as join5 } from "path";
3553
3791
  import { homedir as homedir4 } from "os";
3554
3792
  import { createInterface as createInterface2 } from "readline";
3793
+ function readDirectoryCache() {
3794
+ try {
3795
+ const entry = JSON.parse(readFileSync5(DIRECTORY_CACHE_FILE, "utf8"));
3796
+ if (typeof entry.ts === "number" && Number.isFinite(entry.ts) && Date.now() - entry.ts < INDEX_TTL_MS2) {
3797
+ return { index: entry.index, ts: entry.ts };
3798
+ }
3799
+ return null;
3800
+ } catch {
3801
+ return null;
3802
+ }
3803
+ }
3804
+ function writeDirectoryCache(index) {
3805
+ mkdirSync4(TERMINALHIRE_DIR4, { recursive: true });
3806
+ writeFileSync4(DIRECTORY_CACHE_FILE, JSON.stringify({ ts: Date.now(), index }), "utf8");
3807
+ }
3808
+ function readProject() {
3809
+ try {
3810
+ return JSON.parse(readFileSync5(PROJECT_FILE, "utf8"));
3811
+ } catch {
3812
+ return null;
3813
+ }
3814
+ }
3815
+ function relativeTime(ts) {
3816
+ const secs = Math.max(0, Math.round((Date.now() - ts) / 1e3));
3817
+ if (secs < 60) return `${secs}s ago`;
3818
+ const mins = Math.round(secs / 60);
3819
+ return mins < 60 ? `${mins}m ago` : `${Math.round(mins / 60)}h ago`;
3820
+ }
3821
+ async function fetchDirectory() {
3822
+ const cached = readDirectoryCache();
3823
+ if (cached) {
3824
+ console.log(`\u2713 Using cached directory (updated ${relativeTime(cached.ts)})`);
3825
+ return cached.index;
3826
+ }
3827
+ console.log(`\u21BB Refreshing builder directory from ${API_URL2}/api/directory...`);
3828
+ const res = await fetch(`${API_URL2}/api/directory`, { signal: AbortSignal.timeout(1e4) });
3829
+ if (!res.ok) throw new Error(`/api/directory returned ${res.status}`);
3830
+ const index = await res.json();
3831
+ writeDirectoryCache(index);
3832
+ return index;
3833
+ }
3834
+ function reportMatched(results, fetchImpl = fetch) {
3835
+ try {
3836
+ const logins = [
3837
+ ...new Set(
3838
+ results.map((r) => r?.job?.company).filter((login) => typeof login === "string" && login.length > 0)
3839
+ )
3840
+ ];
3841
+ if (logins.length === 0) return;
3842
+ Promise.resolve(
3843
+ fetchImpl(`${API_URL2}/api/directory/matched`, {
3844
+ method: "POST",
3845
+ headers: { "Content-Type": "application/json" },
3846
+ body: JSON.stringify({ matched: logins }),
3847
+ signal: AbortSignal.timeout(3e3)
3848
+ })
3849
+ ).catch(() => {
3850
+ });
3851
+ } catch {
3852
+ }
3853
+ }
3854
+ function prompt2(question) {
3855
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
3856
+ return new Promise((resolve2) => {
3857
+ rl.question(question, (answer) => {
3858
+ rl.close();
3859
+ resolve2(answer.trim().toLowerCase());
3860
+ });
3861
+ });
3862
+ }
3863
+ function credentialUrl(job) {
3864
+ const u = job.url ?? "";
3865
+ return u.startsWith("http") ? u : `${API_URL2}${u}`;
3866
+ }
3867
+ function linkTitle2(title, url) {
3868
+ const isTTY = process.stdout.isTTY;
3869
+ const noColor = process.env["NO_COLOR"] !== void 0;
3870
+ if (isTTY && !noColor && url) return `\x1B]8;;${url}\x1B\\${title}\x1B]8;;\x1B\\`;
3871
+ return url ? `${title} (${url})` : title;
3872
+ }
3873
+ function printCard(i, result) {
3874
+ const { job, score, matchedTags, reason } = result;
3875
+ const url = credentialUrl(job);
3876
+ const kind = job.source === "project" ? "project" : "developer";
3877
+ const byline = job.source === "project" ? ` \xB7 by @${job.company}` : "";
3878
+ const scoreStr = score > 0 ? ` \xB7 match ${Math.round(score * 100)}%` : "";
3879
+ console.log(`
3880
+ ${i + 1}. ${linkTitle2(job.title, url)} \u2014 ${kind}${byline}${scoreStr}`);
3881
+ console.log(` id: ${job.id}`);
3882
+ if (reason) console.log(` ${reason}`);
3883
+ if (matchedTags && matchedTags.length) console.log(` Tags matched: ${matchedTags.slice(0, 5).join(", ")}`);
3884
+ console.log(` Profile: ${url}`);
3885
+ }
3886
+ async function run3() {
3887
+ try {
3888
+ const { match: match2 } = await Promise.resolve().then(() => (init_src(), src_exports));
3889
+ let fp;
3890
+ if (AS_PROJECT) {
3891
+ const project = readProject();
3892
+ if (!project) {
3893
+ console.log("\u2726 terminalhire devs --as-project: no project declared yet.");
3894
+ console.log(' Run `terminalhire project "<title>: <skills it needs>"` first, then come back.');
3895
+ return;
3896
+ }
3897
+ if (!project.skillTags || project.skillTags.length === 0) {
3898
+ console.log("\u2726 Your declared project has no recognized skills yet.");
3899
+ console.log(" Re-run `terminalhire project` with known stack names (react, go, postgres, ...).");
3900
+ return;
3901
+ }
3902
+ fp = { skillTags: project.skillTags, prefs: project.prefs ?? {} };
3903
+ console.log(`Ranking builders for your project: ${project.title}`);
3904
+ } else {
3905
+ const { readProfile: readProfile2, profileToFingerprint: profileToFingerprint2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
3906
+ const profile = await readProfile2();
3907
+ if (profile.skillTags.length === 0) {
3908
+ console.log("\u2726 terminalhire devs: no skill tags in your local profile yet.");
3909
+ console.log(" Run `terminalhire init` first to set up your profile, then come back.");
3910
+ console.log(" (Tags also accumulate as you work in Claude Code sessions.)");
3911
+ return;
3912
+ }
3913
+ fp = profileToFingerprint2(profile);
3914
+ }
3915
+ const index = await fetchDirectory();
3916
+ const cards = index.cards ?? [];
3917
+ if (cards.length === 0) {
3918
+ console.log("\nNo builders or projects published yet. Check back soon \u2014 the directory fills as devs publish.");
3919
+ return;
3920
+ }
3921
+ const results = match2(fp, cards, SHOW_ALL2 ? cards.length : LIMIT2);
3922
+ if (results.length === 0) {
3923
+ console.log(`No matching builders or projects for your current ${AS_PROJECT ? "project" : "profile"}.`);
3924
+ console.log(" Your tags: " + fp.skillTags.join(", "));
3925
+ return;
3926
+ }
3927
+ reportMatched(results);
3928
+ console.log(
3929
+ `
3930
+ \u2726 ${results.length} match${results.length === 1 ? "" : "es"} in the builder directory (local match \u2014 no data sent)
3931
+ `
3932
+ );
3933
+ for (let i = 0; i < results.length; i++) {
3934
+ printCard(i, results[i]);
3935
+ }
3936
+ if (!process.stdin.isTTY) return;
3937
+ console.log("\n" + "\u2500".repeat(70));
3938
+ const pick = await prompt2(`
3939
+ Enter a number to open a profile, or press Enter to exit: `);
3940
+ const idx = parseInt(pick, 10) - 1;
3941
+ if (Number.isNaN(idx) || idx < 0 || idx >= results.length) return;
3942
+ console.log(`
3943
+ Open this public credential (no data shared):
3944
+ ${credentialUrl(results[idx].job)}`);
3945
+ } catch (err) {
3946
+ console.error("terminalhire devs error:", err.message ?? err);
3947
+ process.exit(1);
3948
+ }
3949
+ }
3950
+ var TERMINALHIRE_DIR4, DIRECTORY_CACHE_FILE, PROJECT_FILE, INDEX_TTL_MS2, API_URL2, DEFAULT_LIMIT2, args2, limitArg2, LIMIT2, SHOW_ALL2, AS_PROJECT;
3951
+ var init_jpi_devs = __esm({
3952
+ "bin/jpi-devs.js"() {
3953
+ "use strict";
3954
+ TERMINALHIRE_DIR4 = join5(homedir4(), ".terminalhire");
3955
+ DIRECTORY_CACHE_FILE = join5(TERMINALHIRE_DIR4, "directory-cache.json");
3956
+ PROJECT_FILE = join5(TERMINALHIRE_DIR4, "project.json");
3957
+ INDEX_TTL_MS2 = 15 * 60 * 1e3;
3958
+ API_URL2 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
3959
+ DEFAULT_LIMIT2 = 10;
3960
+ args2 = process.argv.slice(2);
3961
+ limitArg2 = args2.indexOf("--limit");
3962
+ LIMIT2 = limitArg2 !== -1 ? parseInt(args2[limitArg2 + 1] ?? "10", 10) : DEFAULT_LIMIT2;
3963
+ SHOW_ALL2 = args2.includes("--all");
3964
+ AS_PROJECT = args2.includes("--as-project");
3965
+ }
3966
+ });
3967
+
3968
+ // bin/jpi-project.js
3969
+ var jpi_project_exports = {};
3970
+ __export(jpi_project_exports, {
3971
+ run: () => run4
3972
+ });
3973
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5 } from "fs";
3974
+ import { join as join6 } from "path";
3975
+ import { homedir as homedir5 } from "os";
3976
+ import { createInterface as createInterface3 } from "readline";
3977
+ function readProject2() {
3978
+ try {
3979
+ return JSON.parse(readFileSync6(PROJECT_FILE2, "utf8"));
3980
+ } catch {
3981
+ return null;
3982
+ }
3983
+ }
3984
+ function promptRaw(question) {
3985
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
3986
+ return new Promise((resolve2) => {
3987
+ rl.question(question, (answer) => {
3988
+ rl.close();
3989
+ resolve2(answer.trim());
3990
+ });
3991
+ });
3992
+ }
3993
+ function splitDeclaration(line) {
3994
+ const m = line.match(/\s*[::—]|\s-\s/);
3995
+ if (m && m.index !== void 0) {
3996
+ const title = line.slice(0, m.index).trim();
3997
+ const skillsRaw = line.slice(m.index + m[0].length).trim();
3998
+ return { title: title || line, skillsRaw: skillsRaw || line };
3999
+ }
4000
+ return { title: line, skillsRaw: line };
4001
+ }
4002
+ function tokenize4(skillsRaw) {
4003
+ return skillsRaw.split(/[,/]|\s+/).map((t) => t.trim()).filter(Boolean);
4004
+ }
4005
+ async function run4() {
4006
+ try {
4007
+ if (SHOW) {
4008
+ const existing = readProject2();
4009
+ if (!existing) {
4010
+ console.log("\u2726 terminalhire project: nothing declared yet.");
4011
+ console.log(' Run `terminalhire project "<title>: <skills it needs>"` to declare one.');
4012
+ return;
4013
+ }
4014
+ console.log(`
4015
+ \u2726 Your project (local only \u2014 never sent):
4016
+ `);
4017
+ console.log(` ${existing.title}`);
4018
+ console.log(` Skills: ${(existing.skillTags ?? []).join(", ") || "(none recognized)"}`);
4019
+ console.log(` Declared: ${existing.createdAt ?? "unknown"}`);
4020
+ console.log(`
4021
+ Rank builders for it: terminalhire devs --as-project`);
4022
+ return;
4023
+ }
4024
+ const { normalize: normalize2 } = await Promise.resolve().then(() => (init_src(), src_exports));
4025
+ let declaration = declarationArg;
4026
+ if (!declaration) {
4027
+ if (!process.stdin.isTTY) {
4028
+ console.log("\u2726 terminalhire project: declare what you are building, e.g.");
4029
+ console.log(' terminalhire project "Realtime collab editor: react, typescript, websockets, postgres"');
4030
+ return;
4031
+ }
4032
+ console.log("Declare your project in one line \u2014 a short title and the stack/skills it needs.");
4033
+ console.log("Example: Realtime collab editor: react, typescript, websockets, postgres\n");
4034
+ declaration = await promptRaw("Project: ");
4035
+ }
4036
+ if (!declaration) {
4037
+ console.log("Nothing declared \u2014 run again with a one-line project.");
4038
+ return;
4039
+ }
4040
+ const { title, skillsRaw } = splitDeclaration(declaration);
4041
+ const skillTags = normalize2(tokenize4(skillsRaw));
4042
+ if (skillTags.length === 0) {
4043
+ console.log("\n\u2726 No recognized skills in that declaration.");
4044
+ console.log(" Use known stack names (e.g. react, typescript, go, postgres, kubernetes).");
4045
+ console.log(" Nothing was saved.");
4046
+ return;
4047
+ }
4048
+ const project = {
4049
+ title,
4050
+ declaration,
4051
+ skillTags,
4052
+ prefs: {},
4053
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
4054
+ };
4055
+ mkdirSync5(TERMINALHIRE_DIR5, { recursive: true });
4056
+ writeFileSync5(PROJECT_FILE2, JSON.stringify(project, null, 2), "utf8");
4057
+ console.log(`
4058
+ \u2726 Project saved locally (never sent): ${title}`);
4059
+ console.log(` Skills: ${skillTags.join(", ")}`);
4060
+ console.log(`
4061
+ Rank builders for it: terminalhire devs --as-project`);
4062
+ } catch (err) {
4063
+ console.error("terminalhire project error:", err.message ?? err);
4064
+ process.exit(1);
4065
+ }
4066
+ }
4067
+ var TERMINALHIRE_DIR5, PROJECT_FILE2, args3, SHOW, declarationArg;
4068
+ var init_jpi_project = __esm({
4069
+ "bin/jpi-project.js"() {
4070
+ "use strict";
4071
+ TERMINALHIRE_DIR5 = join6(homedir5(), ".terminalhire");
4072
+ PROJECT_FILE2 = join6(TERMINALHIRE_DIR5, "project.json");
4073
+ args3 = process.argv.slice(2);
4074
+ SHOW = args3.includes("--show");
4075
+ declarationArg = args3.filter((a) => !a.startsWith("--")).join(" ").trim();
4076
+ }
4077
+ });
4078
+
4079
+ // bin/jpi-bounties.js
4080
+ var jpi_bounties_exports = {};
4081
+ __export(jpi_bounties_exports, {
4082
+ run: () => run5
4083
+ });
4084
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6 } from "fs";
4085
+ import { join as join7 } from "path";
4086
+ import { homedir as homedir6 } from "os";
4087
+ import { createInterface as createInterface4 } from "readline";
3555
4088
  function readIndexCache2() {
3556
4089
  try {
3557
- const entry = JSON.parse(readFileSync5(INDEX_CACHE_FILE2, "utf8"));
3558
- if (Date.now() - entry.ts < INDEX_TTL_MS2) return entry.index;
4090
+ const entry = JSON.parse(readFileSync7(INDEX_CACHE_FILE2, "utf8"));
4091
+ if (Date.now() - entry.ts < INDEX_TTL_MS3) return entry.index;
3559
4092
  return null;
3560
4093
  } catch {
3561
4094
  return null;
3562
4095
  }
3563
4096
  }
3564
4097
  function writeIndexCache2(index) {
3565
- mkdirSync4(TERMINALHIRE_DIR4, { recursive: true });
3566
- writeFileSync4(INDEX_CACHE_FILE2, JSON.stringify({ ts: Date.now(), index }), "utf8");
4098
+ mkdirSync6(TERMINALHIRE_DIR6, { recursive: true });
4099
+ writeFileSync6(INDEX_CACHE_FILE2, JSON.stringify({ ts: Date.now(), index }), "utf8");
3567
4100
  }
3568
4101
  async function fetchIndex2() {
3569
4102
  const cached = readIndexCache2();
3570
4103
  if (cached) return cached;
3571
- const res = await fetch(`${API_URL2}/api/index`, { signal: AbortSignal.timeout(1e4) });
4104
+ const res = await fetch(`${API_URL3}/api/index`, { signal: AbortSignal.timeout(1e4) });
3572
4105
  if (!res.ok) throw new Error(`/api/index returned ${res.status}`);
3573
4106
  const index = await res.json();
3574
4107
  writeIndexCache2(index);
3575
4108
  return index;
3576
4109
  }
3577
- function prompt2(question) {
3578
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
4110
+ function prompt3(question) {
4111
+ const rl = createInterface4({ input: process.stdin, output: process.stdout });
3579
4112
  return new Promise((resolve2) => {
3580
4113
  rl.question(question, (answer) => {
3581
4114
  rl.close();
@@ -3586,7 +4119,7 @@ function prompt2(question) {
3586
4119
  function formatAmount(b) {
3587
4120
  return b.amountUSD != null ? "$" + b.amountUSD.toLocaleString() : "$\u2014";
3588
4121
  }
3589
- function linkTitle2(title, url) {
4122
+ function linkTitle3(title, url) {
3590
4123
  const isTTY = process.stdout.isTTY;
3591
4124
  const noColor = process.env["NO_COLOR"] !== void 0;
3592
4125
  if (isTTY && !noColor && url) return `\x1B]8;;${url}\x1B\\${title}\x1B]8;;\x1B\\`;
@@ -3600,16 +4133,16 @@ function printBounty(i, job, score, reason, matchedTags) {
3600
4133
  const prs = b.competingOpenPRs;
3601
4134
  const contend = prs != null && prs > 0 ? ` \xB7 \u26A0 ${prs} PR${prs === 1 ? "" : "s"} in flight` : "";
3602
4135
  console.log(`
3603
- ${i + 1}. ${linkTitle2(job.title, job.url)}`);
4136
+ ${i + 1}. ${linkTitle3(job.title, job.url)}`);
3604
4137
  console.log(` ${formatAmount(b)}${effort} \xB7 ${b.repoFullName ?? job.company}${stars}${scoreStr}${contend}`);
3605
4138
  if (reason) console.log(` ${reason}`);
3606
4139
  if (matchedTags && matchedTags.length) console.log(` Tags matched: ${matchedTags.slice(0, 5).join(", ")}`);
3607
4140
  console.log(` id: ${job.id}`);
3608
4141
  console.log(` Claim: ${b.claimUrl ?? job.url}`);
3609
4142
  }
3610
- async function run3() {
4143
+ async function run5() {
3611
4144
  try {
3612
- console.log(`Fetching bounty index from ${API_URL2}/api/index...`);
4145
+ console.log(`Fetching bounty index from ${API_URL3}/api/index...`);
3613
4146
  const index = await fetchIndex2();
3614
4147
  let bounties = (index.jobs ?? []).filter((j) => j.source === "bounty");
3615
4148
  if (PRICED_ONLY) bounties = bounties.filter((j) => j.bounty?.amountUSD != null);
@@ -3635,7 +4168,7 @@ async function run3() {
3635
4168
  const amt = (j) => j.bounty?.amountUSD ?? -1;
3636
4169
  const contested = (j) => (j.bounty?.competingOpenPRs ?? 0) > 0 ? 1 : 0;
3637
4170
  bounties.sort((a, b) => contested(a) - contested(b) || score(b) - score(a) || amt(b) - amt(a));
3638
- const shown = SHOW_ALL2 ? bounties : bounties.slice(0, LIMIT2);
4171
+ const shown = SHOW_ALL3 ? bounties : bounties.slice(0, LIMIT3);
3639
4172
  const matchedCount = bounties.filter((j) => score(j) > 0).length;
3640
4173
  console.log(
3641
4174
  `
@@ -3646,13 +4179,13 @@ async function run3() {
3646
4179
  const r = ranked.get(shown[i].id);
3647
4180
  printBounty(i, shown[i], r?.score ?? 0, r?.reason, r?.matchedTags);
3648
4181
  }
3649
- if (!SHOW_ALL2 && bounties.length > shown.length) {
4182
+ if (!SHOW_ALL3 && bounties.length > shown.length) {
3650
4183
  console.log(`
3651
4184
  \u2026and ${bounties.length - shown.length} more \u2014 run with --all to see every bounty.`);
3652
4185
  }
3653
4186
  if (!process.stdin.isTTY) return;
3654
4187
  console.log("\n" + "\u2500".repeat(70));
3655
- const pick = await prompt2(`
4188
+ const pick = await prompt3(`
3656
4189
  Enter a number to open a bounty's claim page, or press Enter to exit: `);
3657
4190
  const idx = parseInt(pick, 10) - 1;
3658
4191
  if (Number.isNaN(idx) || idx < 0 || idx >= shown.length) return;
@@ -3667,21 +4200,21 @@ Open this to claim/work the bounty (you go straight to the source \u2014 we neve
3667
4200
  process.exit(1);
3668
4201
  }
3669
4202
  }
3670
- var TERMINALHIRE_DIR4, INDEX_CACHE_FILE2, INDEX_TTL_MS2, API_URL2, DEFAULT_LIMIT2, args2, limitArg2, LIMIT2, PRICED_ONLY, SHOW_ALL2, WINNABLE_ONLY, EFFORT_LABEL;
4203
+ var TERMINALHIRE_DIR6, INDEX_CACHE_FILE2, INDEX_TTL_MS3, API_URL3, DEFAULT_LIMIT3, args4, limitArg3, LIMIT3, PRICED_ONLY, SHOW_ALL3, WINNABLE_ONLY, EFFORT_LABEL;
3671
4204
  var init_jpi_bounties = __esm({
3672
4205
  "bin/jpi-bounties.js"() {
3673
4206
  "use strict";
3674
- TERMINALHIRE_DIR4 = join5(homedir4(), ".terminalhire");
3675
- INDEX_CACHE_FILE2 = join5(TERMINALHIRE_DIR4, "index-cache.json");
3676
- INDEX_TTL_MS2 = 15 * 60 * 1e3;
3677
- API_URL2 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
3678
- DEFAULT_LIMIT2 = 15;
3679
- args2 = process.argv.slice(2);
3680
- limitArg2 = args2.indexOf("--limit");
3681
- LIMIT2 = limitArg2 !== -1 ? parseInt(args2[limitArg2 + 1] ?? "15", 10) : DEFAULT_LIMIT2;
3682
- PRICED_ONLY = args2.includes("--priced");
3683
- SHOW_ALL2 = args2.includes("--all");
3684
- WINNABLE_ONLY = args2.includes("--winnable");
4207
+ TERMINALHIRE_DIR6 = join7(homedir6(), ".terminalhire");
4208
+ INDEX_CACHE_FILE2 = join7(TERMINALHIRE_DIR6, "index-cache.json");
4209
+ INDEX_TTL_MS3 = 15 * 60 * 1e3;
4210
+ API_URL3 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
4211
+ DEFAULT_LIMIT3 = 15;
4212
+ args4 = process.argv.slice(2);
4213
+ limitArg3 = args4.indexOf("--limit");
4214
+ LIMIT3 = limitArg3 !== -1 ? parseInt(args4[limitArg3 + 1] ?? "15", 10) : DEFAULT_LIMIT3;
4215
+ PRICED_ONLY = args4.includes("--priced");
4216
+ SHOW_ALL3 = args4.includes("--all");
4217
+ WINNABLE_ONLY = args4.includes("--winnable");
3685
4218
  EFFORT_LABEL = { small: "small (~\xBD day)", medium: "medium (~1 day)", large: "large (multi-day)" };
3686
4219
  }
3687
4220
  });
@@ -3697,26 +4230,26 @@ __export(claims_exports, {
3697
4230
  removeClaim: () => removeClaim,
3698
4231
  updateClaim: () => updateClaim
3699
4232
  });
3700
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, renameSync, existsSync as existsSync4 } from "fs";
3701
- import { join as join6 } from "path";
3702
- import { homedir as homedir5 } from "os";
4233
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, renameSync, existsSync as existsSync4 } from "fs";
4234
+ import { join as join8 } from "path";
4235
+ import { homedir as homedir7 } from "os";
3703
4236
  function nowISO() {
3704
4237
  return (/* @__PURE__ */ new Date()).toISOString();
3705
4238
  }
3706
4239
  function readClaims() {
3707
4240
  try {
3708
4241
  if (!existsSync4(CLAIMS_FILE)) return [];
3709
- const data = JSON.parse(readFileSync6(CLAIMS_FILE, "utf8"));
4242
+ const data = JSON.parse(readFileSync8(CLAIMS_FILE, "utf8"));
3710
4243
  return Array.isArray(data?.claims) ? data.claims : [];
3711
4244
  } catch {
3712
4245
  return [];
3713
4246
  }
3714
4247
  }
3715
4248
  function writeClaims(claims) {
3716
- mkdirSync5(TERMINALHIRE_DIR5, { recursive: true });
4249
+ mkdirSync7(TERMINALHIRE_DIR7, { recursive: true });
3717
4250
  const tmp = `${CLAIMS_FILE}.tmp`;
3718
4251
  const payload = { claims };
3719
- writeFileSync5(tmp, JSON.stringify(payload, null, 2), "utf8");
4252
+ writeFileSync7(tmp, JSON.stringify(payload, null, 2), "utf8");
3720
4253
  renameSync(tmp, CLAIMS_FILE);
3721
4254
  }
3722
4255
  function findClaim(id) {
@@ -3769,12 +4302,12 @@ function acceptedPRRate(claims = readClaims()) {
3769
4302
  const merged = claims.filter((c) => c.state === "merged").length;
3770
4303
  return { merged, total, rate: total === 0 ? 0 : merged / total };
3771
4304
  }
3772
- var TERMINALHIRE_DIR5, CLAIMS_FILE, TERMINAL_STATES;
4305
+ var TERMINALHIRE_DIR7, CLAIMS_FILE, TERMINAL_STATES;
3773
4306
  var init_claims = __esm({
3774
4307
  "src/claims.ts"() {
3775
4308
  "use strict";
3776
- TERMINALHIRE_DIR5 = join6(homedir5(), ".terminalhire");
3777
- CLAIMS_FILE = join6(TERMINALHIRE_DIR5, "claims.json");
4309
+ TERMINALHIRE_DIR7 = join8(homedir7(), ".terminalhire");
4310
+ CLAIMS_FILE = join8(TERMINALHIRE_DIR7, "claims.json");
3778
4311
  TERMINAL_STATES = /* @__PURE__ */ new Set(["merged", "abandoned"]);
3779
4312
  }
3780
4313
  });
@@ -3782,20 +4315,20 @@ var init_claims = __esm({
3782
4315
  // bin/jpi-claim.js
3783
4316
  var jpi_claim_exports = {};
3784
4317
  __export(jpi_claim_exports, {
3785
- run: () => run4
4318
+ run: () => run6
3786
4319
  });
3787
- import { readFileSync as readFileSync7, existsSync as existsSync5 } from "fs";
3788
- import { join as join7 } from "path";
3789
- import { homedir as homedir6 } from "os";
4320
+ import { readFileSync as readFileSync9, existsSync as existsSync5 } from "fs";
4321
+ import { join as join9 } from "path";
4322
+ import { homedir as homedir8 } from "os";
3790
4323
  import { execFile } from "child_process";
3791
4324
  import { promisify } from "util";
3792
- import { createInterface as createInterface3 } from "readline";
3793
- async function sh(cmd, args3, opts = {}) {
3794
- const { stdout } = await pExecFile(cmd, args3, { ...opts, shell: false, maxBuffer: 16 * 1024 * 1024 });
4325
+ import { createInterface as createInterface5 } from "readline";
4326
+ async function sh(cmd, args5, opts = {}) {
4327
+ const { stdout } = await pExecFile(cmd, args5, { ...opts, shell: false, maxBuffer: 16 * 1024 * 1024 });
3795
4328
  return String(stdout).trim();
3796
4329
  }
3797
4330
  async function confirm(question) {
3798
- const rl = createInterface3({ input: process.stdin, output: process.stdout });
4331
+ const rl = createInterface5({ input: process.stdin, output: process.stdout });
3799
4332
  try {
3800
4333
  const ans = await new Promise((resolve2) => rl.question(question, resolve2));
3801
4334
  return /^y(es)?$/i.test(String(ans).trim());
@@ -3834,7 +4367,7 @@ function parseRepoFromRemote(url) {
3834
4367
  function findBountyInCache(bountyId) {
3835
4368
  try {
3836
4369
  if (!existsSync5(INDEX_CACHE_FILE3)) return null;
3837
- const entry = JSON.parse(readFileSync7(INDEX_CACHE_FILE3, "utf8"));
4370
+ const entry = JSON.parse(readFileSync9(INDEX_CACHE_FILE3, "utf8"));
3838
4371
  const jobs = entry?.index?.jobs ?? [];
3839
4372
  const job = jobs.find((j) => j.id === bountyId && j.source === "bounty");
3840
4373
  return job ?? null;
@@ -4298,7 +4831,7 @@ async function cmdSubmit(id, worktreeOverride) {
4298
4831
  \u2713 Submitted ${id} \u2192 ${prUrl}`);
4299
4832
  console.log(` Run 'terminalhire claim status ${id}' after the maintainer acts to fold the merge into your accepted-PR rate.`);
4300
4833
  }
4301
- async function run4() {
4834
+ async function run6() {
4302
4835
  const verb = process.argv[2];
4303
4836
  const { flags, positional } = parseArgs(process.argv.slice(3));
4304
4837
  const active = Boolean(flags.active);
@@ -4338,12 +4871,12 @@ async function run4() {
4338
4871
  process.exit(1);
4339
4872
  }
4340
4873
  }
4341
- var TERMINALHIRE_DIR6, INDEX_CACHE_FILE3, GH_API, GH_HEADERS, pExecFile, VALUE_FLAGS;
4874
+ var TERMINALHIRE_DIR8, INDEX_CACHE_FILE3, GH_API, GH_HEADERS, pExecFile, VALUE_FLAGS;
4342
4875
  var init_jpi_claim = __esm({
4343
4876
  "bin/jpi-claim.js"() {
4344
4877
  "use strict";
4345
- TERMINALHIRE_DIR6 = join7(homedir6(), ".terminalhire");
4346
- INDEX_CACHE_FILE3 = join7(TERMINALHIRE_DIR6, "index-cache.json");
4878
+ TERMINALHIRE_DIR8 = join9(homedir8(), ".terminalhire");
4879
+ INDEX_CACHE_FILE3 = join9(TERMINALHIRE_DIR8, "index-cache.json");
4347
4880
  GH_API = "https://api.github.com";
4348
4881
  GH_HEADERS = { "User-Agent": "terminalhire-claim", Accept: "application/vnd.github+json" };
4349
4882
  pExecFile = promisify(execFile);
@@ -4627,7 +5160,7 @@ function finalize(build) {
4627
5160
  };
4628
5161
  }
4629
5162
  function reconstruct(files, opts = {}) {
4630
- const join18 = opts.joinSidechains !== false;
5163
+ const join20 = opts.joinSidechains !== false;
4631
5164
  const mains = [];
4632
5165
  const sidechains = [];
4633
5166
  for (const file of files) {
@@ -4652,7 +5185,7 @@ function reconstruct(files, opts = {}) {
4652
5185
  }
4653
5186
  const orphanedSidechainPaths = [];
4654
5187
  const joinedPaths = /* @__PURE__ */ new Set();
4655
- if (join18) {
5188
+ if (join20) {
4656
5189
  const sidechainsBySession = /* @__PURE__ */ new Map();
4657
5190
  for (const sc of sidechains) {
4658
5191
  const acc = sidechainsBySession.get(sc.sessionId) ?? [];
@@ -5139,12 +5672,12 @@ function deriveRecoveryDepth(episodes, nodesByUuid) {
5139
5672
  continue;
5140
5673
  }
5141
5674
  spansConsidered++;
5142
- let run14 = 0;
5675
+ let run17 = 0;
5143
5676
  const closeChain = () => {
5144
- if (run14 > 0) {
5677
+ if (run17 > 0) {
5145
5678
  recoveryChains++;
5146
- totalDepth += run14;
5147
- run14 = 0;
5679
+ totalDepth += run17;
5680
+ run17 = 0;
5148
5681
  }
5149
5682
  };
5150
5683
  for (const uuid of episode.mainNodeUuids) {
@@ -5153,9 +5686,9 @@ function deriveRecoveryDepth(episodes, nodesByUuid) {
5153
5686
  continue;
5154
5687
  }
5155
5688
  if (node.kind === "tool_result" /* ToolResult */ && node.isError) {
5156
- run14++;
5157
- if (run14 > maxConsecutiveErrors) {
5158
- maxConsecutiveErrors = run14;
5689
+ run17++;
5690
+ if (run17 > maxConsecutiveErrors) {
5691
+ maxConsecutiveErrors = run17;
5159
5692
  }
5160
5693
  } else if (node.kind === "tool_result" /* ToolResult */ && !node.isError) {
5161
5694
  closeChain();
@@ -5207,13 +5740,13 @@ __export(trajectory_exports, {
5207
5740
  });
5208
5741
  import {
5209
5742
  existsSync as existsSync6,
5210
- mkdirSync as mkdirSync6,
5211
- readFileSync as readFileSync8,
5743
+ mkdirSync as mkdirSync8,
5744
+ readFileSync as readFileSync10,
5212
5745
  readdirSync,
5213
- writeFileSync as writeFileSync6
5746
+ writeFileSync as writeFileSync8
5214
5747
  } from "fs";
5215
- import { homedir as homedir7 } from "os";
5216
- import { join as join8 } from "path";
5748
+ import { homedir as homedir9 } from "os";
5749
+ import { join as join10 } from "path";
5217
5750
  function isRecord4(value) {
5218
5751
  return typeof value === "object" && value !== null && !Array.isArray(value);
5219
5752
  }
@@ -5245,7 +5778,7 @@ function findJsonlFiles(dir) {
5245
5778
  return out;
5246
5779
  }
5247
5780
  for (const entry of entries) {
5248
- const full = join8(dir, entry.name);
5781
+ const full = join10(dir, entry.name);
5249
5782
  if (entry.isDirectory()) {
5250
5783
  out.push(...findJsonlFiles(full));
5251
5784
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -5259,7 +5792,7 @@ function loadCorpus(paths) {
5259
5792
  for (const path of paths) {
5260
5793
  let text;
5261
5794
  try {
5262
- text = readFileSync8(path, "utf8");
5795
+ text = readFileSync10(path, "utf8");
5263
5796
  } catch {
5264
5797
  continue;
5265
5798
  }
@@ -5358,12 +5891,12 @@ function renderMarkdown(view) {
5358
5891
  return lines.join("\n");
5359
5892
  }
5360
5893
  function writeExportArtifacts(score, markdown) {
5361
- const dir = join8(homedir7(), ".terminalhire");
5362
- mkdirSync6(dir, { recursive: true });
5363
- const jsonPath = join8(dir, "trajectory-export.json");
5364
- const mdPath = join8(dir, "trajectory-export.md");
5365
- writeFileSync6(jsonPath, JSON.stringify(score, null, 2) + "\n", "utf8");
5366
- writeFileSync6(mdPath, markdown, "utf8");
5894
+ const dir = join10(homedir9(), ".terminalhire");
5895
+ mkdirSync8(dir, { recursive: true });
5896
+ const jsonPath = join10(dir, "trajectory-export.json");
5897
+ const mdPath = join10(dir, "trajectory-export.md");
5898
+ writeFileSync8(jsonPath, JSON.stringify(score, null, 2) + "\n", "utf8");
5899
+ writeFileSync8(mdPath, markdown, "utf8");
5367
5900
  return { jsonPath, mdPath };
5368
5901
  }
5369
5902
  function renderInward(allNodes, view, files) {
@@ -5382,7 +5915,7 @@ function renderInward(allNodes, view, files) {
5382
5915
  console.log("");
5383
5916
  }
5384
5917
  function buildTrajectory() {
5385
- const projectsDir = join8(homedir7(), ".claude", "projects");
5918
+ const projectsDir = join10(homedir9(), ".claude", "projects");
5386
5919
  if (!existsSync6(projectsDir)) return null;
5387
5920
  const paths = findJsonlFiles(projectsDir);
5388
5921
  if (paths.length === 0) return null;
@@ -5456,8 +5989,8 @@ function defaultPushDeps() {
5456
5989
  }
5457
5990
  },
5458
5991
  prompt: async (question) => {
5459
- const { createInterface: createInterface8 } = await import("readline");
5460
- const rl = createInterface8({ input: process.stdin, output: process.stdout });
5992
+ const { createInterface: createInterface10 } = await import("readline");
5993
+ const rl = createInterface10({ input: process.stdin, output: process.stdout });
5461
5994
  return new Promise((res) => {
5462
5995
  rl.question(question, (answer) => {
5463
5996
  rl.close();
@@ -5465,7 +5998,7 @@ function defaultPushDeps() {
5465
5998
  });
5466
5999
  });
5467
6000
  },
5468
- fetchImpl: (...args3) => globalThis.fetch(...args3),
6001
+ fetchImpl: (...args5) => globalThis.fetch(...args5),
5469
6002
  openBrowser: (url) => {
5470
6003
  void Promise.resolve().then(() => (init_open_url(), open_url_exports)).then((m) => m.openInBrowser(url)).catch(() => {
5471
6004
  });
@@ -5653,19 +6186,19 @@ var init_trajectory = __esm({
5653
6186
  // bin/jpi-trajectory.js
5654
6187
  var jpi_trajectory_exports = {};
5655
6188
  __export(jpi_trajectory_exports, {
5656
- run: () => run5
6189
+ run: () => run7
5657
6190
  });
5658
- async function run5() {
6191
+ async function run7() {
5659
6192
  try {
5660
- const args3 = process.argv.slice(2);
5661
- if (args3.includes("--push")) {
5662
- const doDelete = args3.includes("--delete") || args3.includes("--unlink");
6193
+ const args5 = process.argv.slice(2);
6194
+ if (args5.includes("--push")) {
6195
+ const doDelete = args5.includes("--delete") || args5.includes("--unlink");
5663
6196
  const { runTrajectoryPush: runTrajectoryPush2 } = await Promise.resolve().then(() => (init_trajectory(), trajectory_exports));
5664
6197
  await runTrajectoryPush2({ delete: doDelete });
5665
6198
  return;
5666
6199
  }
5667
- const doExport = args3.includes("--export");
5668
- const inward = args3.includes("--inward");
6200
+ const doExport = args5.includes("--export");
6201
+ const inward = args5.includes("--inward");
5669
6202
  const { runTrajectory: runTrajectory2 } = await Promise.resolve().then(() => (init_trajectory(), trajectory_exports));
5670
6203
  await runTrajectory2({ export: doExport, inward });
5671
6204
  } catch (err) {
@@ -5679,25 +6212,402 @@ var init_jpi_trajectory = __esm({
5679
6212
  }
5680
6213
  });
5681
6214
 
5682
- // bin/jpi-profile.js
5683
- var jpi_profile_exports = {};
5684
- __export(jpi_profile_exports, {
5685
- run: () => run6
6215
+ // src/intro.ts
6216
+ var intro_exports = {};
6217
+ __export(intro_exports, {
6218
+ runIntroDecision: () => runIntroDecision,
6219
+ runIntroList: () => runIntroList,
6220
+ runIntroRequest: () => runIntroRequest
5686
6221
  });
5687
- import { createInterface as createInterface4 } from "readline";
5688
- function prompt3(question) {
5689
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
5690
- return new Promise((resolve2) => {
5691
- rl.question(question, (answer) => {
5692
- rl.close();
5693
- resolve2(answer.trim());
5694
- });
5695
- });
5696
- }
5697
- async function run6() {
6222
+ function defaultIntroDeps() {
6223
+ return {
6224
+ readGithubLogin: async () => {
6225
+ try {
6226
+ const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
6227
+ const profile = await readProfile2();
6228
+ return profile?.github?.login ?? null;
6229
+ } catch {
6230
+ return null;
6231
+ }
6232
+ },
6233
+ readProfileContact: async () => {
6234
+ try {
6235
+ const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
6236
+ const profile = await readProfile2();
6237
+ return { displayName: profile?.displayName, contactEmail: profile?.contactEmail };
6238
+ } catch {
6239
+ return {};
6240
+ }
6241
+ },
6242
+ prompt: async (question) => {
6243
+ const { createInterface: createInterface10 } = await import("readline");
6244
+ const rl = createInterface10({ input: process.stdin, output: process.stdout });
6245
+ return new Promise((res) => {
6246
+ rl.question(question, (answer) => {
6247
+ rl.close();
6248
+ res(answer.trim().toLowerCase());
6249
+ });
6250
+ });
6251
+ },
6252
+ fetchImpl: (...args5) => globalThis.fetch(...args5),
6253
+ openBrowser: (url) => {
6254
+ void Promise.resolve().then(() => (init_open_url(), open_url_exports)).then((m) => m.openInBrowser(url)).catch(() => {
6255
+ });
6256
+ },
6257
+ sessionCookie: () => {
6258
+ const v = process.env["TERMINALHIRE_WEB_SESSION"];
6259
+ return typeof v === "string" && v.length > 0 ? v : null;
6260
+ },
6261
+ log: (msg) => console.log(msg),
6262
+ errorLog: (msg) => console.error(msg),
6263
+ exit: (code) => process.exit(code)
6264
+ };
6265
+ }
6266
+ function renderConsentCard2(payload, deps) {
6267
+ const { log } = deps;
6268
+ log("");
6269
+ log("\u250C\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\u2510");
6270
+ log("\u2502 terminalhire \u2014 request an intro (opt-in) \u2502");
6271
+ log("\u2514\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\u2518");
6272
+ log("");
6273
+ log(" This sends the following \u2014 and ONLY the following \u2014 to terminalhire,");
6274
+ log(` to ask @${payload.targetLogin} for an intro:`);
6275
+ log("");
6276
+ log(` Your GitHub login : @${payload.requesterLogin}`);
6277
+ log(` Your display name : ${payload.requesterDisplayName}`);
6278
+ log(` Your contact : ${payload.requesterContact}`);
6279
+ log(` Note : ${payload.note ?? "(none)"}`);
6280
+ log(` To developer : @${payload.targetLogin}`);
6281
+ log("");
6282
+ log(" What is NEVER sent: your fingerprint, trajectory, repos, or any other");
6283
+ log(" profile field. @" + payload.targetLogin + " sees only your public login");
6284
+ log(" until they accept \u2014 your contact is shared only on their acceptance.");
6285
+ log("");
6286
+ }
6287
+ async function runIntroRequest(args5, overrides) {
6288
+ const deps = { ...defaultIntroDeps(), ...overrides };
6289
+ const targetLogin = args5.targetLogin?.trim();
6290
+ if (!targetLogin) {
6291
+ deps.errorLog('\n Usage: terminalhire intro <github-login> [--note "..."] [--contact <email>] [--name "..."]\n');
6292
+ deps.exit(1);
6293
+ return;
6294
+ }
6295
+ const requesterLogin = await deps.readGithubLogin();
6296
+ if (!requesterLogin) {
6297
+ deps.log("\n Not signed in. Run `terminalhire login` first, then re-run this command.\n");
6298
+ deps.exit(1);
6299
+ return;
6300
+ }
6301
+ if (targetLogin.toLowerCase() === requesterLogin.toLowerCase()) {
6302
+ deps.errorLog("\n You cannot request an intro to yourself.\n");
6303
+ deps.exit(1);
6304
+ return;
6305
+ }
6306
+ const profile = await deps.readProfileContact();
6307
+ const displayName = (args5.name ?? profile.displayName ?? requesterLogin).trim();
6308
+ const contact = (args5.contact ?? profile.contactEmail ?? "").trim();
6309
+ if (!contact) {
6310
+ deps.errorLog("\n No contact on file. Set one with `terminalhire profile --edit`,");
6311
+ deps.errorLog(" or pass `--contact <email-or-handle>`. Nothing was sent.\n");
6312
+ deps.exit(1);
6313
+ return;
6314
+ }
6315
+ const payload = buildIntroPayload({
6316
+ requesterLogin,
6317
+ requesterDisplayName: displayName,
6318
+ requesterContact: contact,
6319
+ targetLogin,
6320
+ note: args5.note
6321
+ });
6322
+ renderConsentCard2(payload, deps);
6323
+ const answer = await deps.prompt(' Type "yes" to send this intro request (anything else cancels): ');
6324
+ if (answer !== "yes") {
6325
+ deps.log("\n Cancelled \u2014 nothing was sent.\n");
6326
+ deps.exit(0);
6327
+ return;
6328
+ }
6329
+ const cookie = deps.sessionCookie();
6330
+ if (!cookie) {
6331
+ deps.log("\n No linked web session found on this machine.");
6332
+ deps.log(" Sign in at your dashboard first, then re-run with a bridged session.");
6333
+ deps.log(` \u2192 ${LINK_BASE2}/dashboard
6334
+ `);
6335
+ deps.openBrowser(`${LINK_BASE2}/dashboard`);
6336
+ deps.exit(0);
6337
+ return;
6338
+ }
6339
+ let res;
6340
+ try {
6341
+ res = await deps.fetchImpl(`${LINK_BASE2}/api/intro/request`, {
6342
+ method: "POST",
6343
+ headers: { "Content-Type": "application/json", Cookie: `${GH_SESSION_COOKIE2}=${cookie}` },
6344
+ body: JSON.stringify(payload),
6345
+ signal: AbortSignal.timeout(1e4)
6346
+ });
6347
+ } catch (err) {
6348
+ deps.errorLog(`
6349
+ Request failed: ${err instanceof Error ? err.message : String(err)}
6350
+ `);
6351
+ deps.exit(1);
6352
+ return;
6353
+ }
6354
+ if (res.status === 401) {
6355
+ deps.log("\n Your web session expired \u2014 sign in again at your dashboard, then re-run.");
6356
+ deps.log(` \u2192 ${LINK_BASE2}/dashboard
6357
+ `);
6358
+ deps.exit(1);
6359
+ return;
6360
+ }
6361
+ if (res.status === 429) {
6362
+ deps.errorLog("\n Too many intro requests right now \u2014 please try again later.\n");
6363
+ deps.exit(1);
6364
+ return;
6365
+ }
6366
+ if (!res.ok) {
6367
+ deps.errorLog(`
6368
+ Request failed: /api/intro/request returned ${res.status}.
6369
+ `);
6370
+ deps.exit(1);
6371
+ return;
6372
+ }
6373
+ deps.log(`
6374
+ Intro request sent to @${targetLogin}. They will see only your public login`);
6375
+ deps.log(" until they accept; your contact is shared only if they do.\n");
6376
+ }
6377
+ async function runIntroDecision(args5, overrides) {
6378
+ const deps = { ...defaultIntroDeps(), ...overrides };
6379
+ const id = args5.id?.trim();
6380
+ if (!id) {
6381
+ deps.errorLog("\n Usage: terminalhire intro --accept <id> | --decline <id>\n");
6382
+ deps.exit(1);
6383
+ return;
6384
+ }
6385
+ const cookie = deps.sessionCookie();
6386
+ if (!cookie) {
6387
+ deps.log("\n No linked web session found on this machine.");
6388
+ deps.log(" Sign in at your dashboard first, then re-run.");
6389
+ deps.log(` \u2192 ${LINK_BASE2}/dashboard
6390
+ `);
6391
+ deps.openBrowser(`${LINK_BASE2}/dashboard`);
6392
+ deps.exit(0);
6393
+ return;
6394
+ }
6395
+ let contact = "";
6396
+ if (args5.action === "accept") {
6397
+ const profile = await deps.readProfileContact();
6398
+ contact = (args5.contact ?? profile.contactEmail ?? "").trim();
6399
+ if (!contact) {
6400
+ deps.errorLog("\n No contact on file to share back. Set one with `terminalhire profile --edit`,");
6401
+ deps.errorLog(" or pass `--contact <email-or-handle>`. Nothing was sent.\n");
6402
+ deps.exit(1);
6403
+ return;
6404
+ }
6405
+ deps.log("");
6406
+ deps.log(" Accepting shares your contact with the requester so they can reach you:");
6407
+ deps.log(` Your contact : ${contact}`);
6408
+ deps.log(" Nothing else is shared. Declining shares nothing.");
6409
+ deps.log("");
6410
+ const answer = await deps.prompt(' Type "yes" to accept and share your contact (anything else cancels): ');
6411
+ if (answer !== "yes") {
6412
+ deps.log("\n Cancelled \u2014 nothing was sent.\n");
6413
+ deps.exit(0);
6414
+ return;
6415
+ }
6416
+ }
6417
+ const body = args5.action === "accept" ? { introId: id, action: "accept", targetContact: contact } : { introId: id, action: "decline" };
6418
+ let res;
6419
+ try {
6420
+ res = await deps.fetchImpl(`${LINK_BASE2}/api/intro/accept`, {
6421
+ method: "POST",
6422
+ headers: { "Content-Type": "application/json", Cookie: `${GH_SESSION_COOKIE2}=${cookie}` },
6423
+ body: JSON.stringify(body),
6424
+ signal: AbortSignal.timeout(1e4)
6425
+ });
6426
+ } catch (err) {
6427
+ deps.errorLog(`
6428
+ Request failed: ${err instanceof Error ? err.message : String(err)}
6429
+ `);
6430
+ deps.exit(1);
6431
+ return;
6432
+ }
6433
+ if (res.status === 401) {
6434
+ deps.log("\n Your web session expired \u2014 sign in again at your dashboard, then re-run.");
6435
+ deps.log(` \u2192 ${LINK_BASE2}/dashboard
6436
+ `);
6437
+ deps.exit(1);
6438
+ return;
6439
+ }
6440
+ if (res.status === 403) {
6441
+ deps.errorLog("\n You are not the developer this intro was sent to.\n");
6442
+ deps.exit(1);
6443
+ return;
6444
+ }
6445
+ if (res.status === 404) {
6446
+ deps.errorLog("\n Intro not found.\n");
6447
+ deps.exit(1);
6448
+ return;
6449
+ }
6450
+ if (!res.ok) {
6451
+ deps.errorLog(`
6452
+ Request failed: /api/intro/accept returned ${res.status}.
6453
+ `);
6454
+ deps.exit(1);
6455
+ return;
6456
+ }
6457
+ let data = {};
6458
+ try {
6459
+ data = await res.json();
6460
+ } catch {
6461
+ }
6462
+ if (args5.action === "decline") {
6463
+ deps.log("\n Declined \u2014 no contact was shared.\n");
6464
+ return;
6465
+ }
6466
+ deps.log("\n Accepted. Your contact was shared with the requester.");
6467
+ if (data.contact) deps.log(` You can reach them at: ${data.contact}`);
6468
+ deps.log("");
6469
+ }
6470
+ async function runIntroList(overrides) {
6471
+ const deps = { ...defaultIntroDeps(), ...overrides };
6472
+ const cookie = deps.sessionCookie();
6473
+ if (!cookie) {
6474
+ deps.log("\n No linked web session found on this machine.");
6475
+ deps.log(" Sign in at your dashboard first, then re-run.");
6476
+ deps.log(` \u2192 ${LINK_BASE2}/dashboard
6477
+ `);
6478
+ deps.openBrowser(`${LINK_BASE2}/dashboard`);
6479
+ deps.exit(0);
6480
+ return;
6481
+ }
6482
+ let res;
6483
+ try {
6484
+ res = await deps.fetchImpl(`${LINK_BASE2}/api/intro/list`, {
6485
+ method: "GET",
6486
+ headers: { Cookie: `${GH_SESSION_COOKIE2}=${cookie}` },
6487
+ signal: AbortSignal.timeout(1e4)
6488
+ });
6489
+ } catch (err) {
6490
+ deps.errorLog(`
6491
+ Request failed: ${err instanceof Error ? err.message : String(err)}
6492
+ `);
6493
+ deps.exit(1);
6494
+ return;
6495
+ }
6496
+ if (res.status === 401) {
6497
+ deps.log("\n Your web session expired \u2014 sign in again at your dashboard, then re-run.");
6498
+ deps.log(` \u2192 ${LINK_BASE2}/dashboard
6499
+ `);
6500
+ deps.exit(1);
6501
+ return;
6502
+ }
6503
+ if (!res.ok) {
6504
+ deps.errorLog(`
6505
+ Request failed: /api/intro/list returned ${res.status}.
6506
+ `);
6507
+ deps.exit(1);
6508
+ return;
6509
+ }
6510
+ let data = {};
6511
+ try {
6512
+ data = await res.json();
6513
+ } catch {
6514
+ }
6515
+ const intros = data.intros ?? [];
6516
+ if (intros.length === 0) {
6517
+ deps.log("\n No intros yet.\n");
6518
+ return;
6519
+ }
6520
+ deps.log("");
6521
+ for (const it of intros) {
6522
+ const dir = it.role === "incoming" ? "from" : "to";
6523
+ deps.log(` [${it.status}] ${dir} @${it.counterpartyLogin}`);
6524
+ if (it.note) deps.log(` note: ${it.note}`);
6525
+ if (it.contact) deps.log(` contact: ${it.contact}`);
6526
+ else if (it.role === "incoming" && it.status === "pending") {
6527
+ deps.log(` \u2192 accept: terminalhire intro --accept ${it.id}`);
6528
+ }
6529
+ }
6530
+ deps.log("");
6531
+ }
6532
+ var LINK_BASE2, GH_SESSION_COOKIE2;
6533
+ var init_intro2 = __esm({
6534
+ "src/intro.ts"() {
6535
+ "use strict";
6536
+ init_src();
6537
+ LINK_BASE2 = process.env["TERMINALHIRE_API_URL"] || "https://www.terminalhire.com";
6538
+ GH_SESSION_COOKIE2 = "__jpi_gh_session";
6539
+ }
6540
+ });
6541
+
6542
+ // bin/jpi-intro.js
6543
+ var jpi_intro_exports = {};
6544
+ __export(jpi_intro_exports, {
6545
+ run: () => run8
6546
+ });
6547
+ function readOption(args5, name) {
6548
+ const i = args5.indexOf(name);
6549
+ if (i === -1) return void 0;
6550
+ const v = args5[i + 1];
6551
+ return typeof v === "string" && !v.startsWith("--") ? v : void 0;
6552
+ }
6553
+ async function run8() {
6554
+ try {
6555
+ const args5 = process.argv.slice(2);
6556
+ if (args5.includes("--list")) {
6557
+ const { runIntroList: runIntroList2 } = await Promise.resolve().then(() => (init_intro2(), intro_exports));
6558
+ await runIntroList2();
6559
+ return;
6560
+ }
6561
+ const acceptId = readOption(args5, "--accept");
6562
+ const declineId = readOption(args5, "--decline");
6563
+ if (acceptId || declineId) {
6564
+ const contact2 = readOption(args5, "--contact");
6565
+ const name2 = readOption(args5, "--name");
6566
+ const { runIntroDecision: runIntroDecision2 } = await Promise.resolve().then(() => (init_intro2(), intro_exports));
6567
+ await runIntroDecision2({
6568
+ id: acceptId ?? declineId,
6569
+ action: acceptId ? "accept" : "decline",
6570
+ contact: contact2,
6571
+ name: name2
6572
+ });
6573
+ return;
6574
+ }
6575
+ const targetLogin = args5.find((a) => !a.startsWith("--"));
6576
+ const note = readOption(args5, "--note");
6577
+ const contact = readOption(args5, "--contact");
6578
+ const name = readOption(args5, "--name");
6579
+ const { runIntroRequest: runIntroRequest2 } = await Promise.resolve().then(() => (init_intro2(), intro_exports));
6580
+ await runIntroRequest2({ targetLogin, note, contact, name });
6581
+ } catch (err) {
6582
+ console.error("terminalhire intro error:", err?.message ?? err);
6583
+ process.exit(1);
6584
+ }
6585
+ }
6586
+ var init_jpi_intro = __esm({
6587
+ "bin/jpi-intro.js"() {
6588
+ "use strict";
6589
+ }
6590
+ });
6591
+
6592
+ // bin/jpi-profile.js
6593
+ var jpi_profile_exports = {};
6594
+ __export(jpi_profile_exports, {
6595
+ run: () => run9
6596
+ });
6597
+ import { createInterface as createInterface6 } from "readline";
6598
+ function prompt4(question) {
6599
+ const rl = createInterface6({ input: process.stdin, output: process.stdout });
6600
+ return new Promise((resolve2) => {
6601
+ rl.question(question, (answer) => {
6602
+ rl.close();
6603
+ resolve2(answer.trim());
6604
+ });
6605
+ });
6606
+ }
6607
+ async function run9() {
5698
6608
  const { readProfile: readProfile2, writeProfile: writeProfile2, deleteProfile: deleteProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
5699
- const args3 = process.argv.slice(2);
5700
- if (args3.includes("--show")) {
6609
+ const args5 = process.argv.slice(2);
6610
+ if (args5.includes("--show")) {
5701
6611
  const profile = await readProfile2();
5702
6612
  console.log("\n\u2726 terminalhire local profile (encrypted at rest \u2014 shown here for your review only)\n");
5703
6613
  console.log(" Skill tags: " + (profile.skillTags.length > 0 ? profile.skillTags.join(", ") : "(none yet)"));
@@ -5727,9 +6637,9 @@ async function run6() {
5727
6637
  console.log("\nThis profile NEVER leaves your machine except in a consented lead payload.");
5728
6638
  return;
5729
6639
  }
5730
- if (args3.includes("--delete")) {
6640
+ if (args5.includes("--delete")) {
5731
6641
  console.log("\nThis will permanently delete your local terminalhire profile and encryption key.");
5732
- const answer = await prompt3('Type "yes" to confirm: ');
6642
+ const answer = await prompt4('Type "yes" to confirm: ');
5733
6643
  if (answer !== "yes") {
5734
6644
  console.log("Aborted.");
5735
6645
  process.exit(0);
@@ -5738,17 +6648,17 @@ async function run6() {
5738
6648
  console.log("Profile and key deleted from ~/.terminalhire/");
5739
6649
  return;
5740
6650
  }
5741
- if (args3.includes("--edit")) {
6651
+ if (args5.includes("--edit")) {
5742
6652
  const profile = await readProfile2();
5743
6653
  console.log("\n\u2726 terminalhire profile editor (press Enter to keep current value)\n");
5744
- const name = await prompt3(`Display name [${profile.displayName ?? "not set"}]: `);
6654
+ const name = await prompt4(`Display name [${profile.displayName ?? "not set"}]: `);
5745
6655
  if (name) profile.displayName = name;
5746
- const email = await prompt3(`Contact email [${profile.contactEmail ?? "not set"}]: `);
6656
+ const email = await prompt4(`Contact email [${profile.contactEmail ?? "not set"}]: `);
5747
6657
  if (email) profile.contactEmail = email;
5748
- const remote = await prompt3(`Remote only? (y/n) [${profile.remoteOnly ? "y" : "n"}]: `);
6658
+ const remote = await prompt4(`Remote only? (y/n) [${profile.remoteOnly ? "y" : "n"}]: `);
5749
6659
  if (remote === "y") profile.remoteOnly = true;
5750
6660
  if (remote === "n") profile.remoteOnly = false;
5751
- const floor = await prompt3(`Comp floor USD [${profile.compFloorUsd ?? "not set"}]: `);
6661
+ const floor = await prompt4(`Comp floor USD [${profile.compFloorUsd ?? "not set"}]: `);
5752
6662
  if (floor && !isNaN(parseInt(floor, 10))) profile.compFloorUsd = parseInt(floor, 10);
5753
6663
  await writeProfile2(profile);
5754
6664
  console.log("\nProfile updated (encrypted at ~/.terminalhire/profile.enc)");
@@ -5767,12 +6677,12 @@ var signal_exports = {};
5767
6677
  __export(signal_exports, {
5768
6678
  extractFingerprint: () => extractFingerprint
5769
6679
  });
5770
- import { readFileSync as readFileSync9, readdirSync as readdirSync2 } from "fs";
6680
+ import { readFileSync as readFileSync11, readdirSync as readdirSync2 } from "fs";
5771
6681
  import { execFileSync } from "child_process";
5772
- import { join as join9 } from "path";
5773
- function safeGit(args3, cwd) {
6682
+ import { join as join11 } from "path";
6683
+ function safeGit(args5, cwd) {
5774
6684
  try {
5775
- return execFileSync("git", ["-C", cwd, ...args3], {
6685
+ return execFileSync("git", ["-C", cwd, ...args5], {
5776
6686
  timeout: 2e3,
5777
6687
  stdio: ["ignore", "pipe", "ignore"]
5778
6688
  }).toString().trim();
@@ -5797,20 +6707,20 @@ function isEmployerContext(cwd) {
5797
6707
  }
5798
6708
  function readJsonSafe(path) {
5799
6709
  try {
5800
- return JSON.parse(readFileSync9(path, "utf8"));
6710
+ return JSON.parse(readFileSync11(path, "utf8"));
5801
6711
  } catch {
5802
6712
  return null;
5803
6713
  }
5804
6714
  }
5805
6715
  function readFileSafe(path) {
5806
6716
  try {
5807
- return readFileSync9(path, "utf8");
6717
+ return readFileSync11(path, "utf8");
5808
6718
  } catch {
5809
6719
  return "";
5810
6720
  }
5811
6721
  }
5812
6722
  function tokensFromPackageJson(cwd) {
5813
- const pkg = readJsonSafe(join9(cwd, "package.json"));
6723
+ const pkg = readJsonSafe(join11(cwd, "package.json"));
5814
6724
  if (!pkg || typeof pkg !== "object") return [];
5815
6725
  const p = pkg;
5816
6726
  const deps = {
@@ -5824,9 +6734,9 @@ function workspaceMemberDirs(cwd) {
5824
6734
  const dirs = [cwd];
5825
6735
  for (const group of ["apps", "packages"]) {
5826
6736
  try {
5827
- const groupDir = join9(cwd, group);
6737
+ const groupDir = join11(cwd, group);
5828
6738
  for (const e of readdirSync2(groupDir, { withFileTypes: true })) {
5829
- if (e.isDirectory() && !e.isSymbolicLink()) dirs.push(join9(groupDir, e.name));
6739
+ if (e.isDirectory() && !e.isSymbolicLink()) dirs.push(join11(groupDir, e.name));
5830
6740
  }
5831
6741
  } catch {
5832
6742
  }
@@ -5834,18 +6744,18 @@ function workspaceMemberDirs(cwd) {
5834
6744
  return dirs;
5835
6745
  }
5836
6746
  function tokensFromRequirementsTxt(cwd) {
5837
- const content = readFileSafe(join9(cwd, "requirements.txt"));
6747
+ const content = readFileSafe(join11(cwd, "requirements.txt"));
5838
6748
  if (!content) return [];
5839
6749
  return content.split("\n").map((l) => l.trim().split(/[>=<!\[;]/)[0].trim().toLowerCase()).filter(Boolean);
5840
6750
  }
5841
6751
  function tokensFromGoMod(cwd) {
5842
- const content = readFileSafe(join9(cwd, "go.mod"));
6752
+ const content = readFileSafe(join11(cwd, "go.mod"));
5843
6753
  if (!content) return [];
5844
6754
  const requires = Array.from(content.matchAll(/^\s+([^\s]+)\s+v/gm)).map((m) => m[1].split("/").pop() ?? "").filter(Boolean);
5845
6755
  return ["go", ...requires];
5846
6756
  }
5847
6757
  function tokensFromCargoToml(cwd) {
5848
- const content = readFileSafe(join9(cwd, "Cargo.toml"));
6758
+ const content = readFileSafe(join11(cwd, "Cargo.toml"));
5849
6759
  if (!content) return [];
5850
6760
  const deps = [];
5851
6761
  let inDeps = false;
@@ -5866,7 +6776,7 @@ function tokensFromFileExtensions(cwd) {
5866
6776
  const tokens = [];
5867
6777
  const scanDirs = [cwd];
5868
6778
  try {
5869
- const srcDir = join9(cwd, "src");
6779
+ const srcDir = join11(cwd, "src");
5870
6780
  readdirSync2(srcDir);
5871
6781
  scanDirs.push(srcDir);
5872
6782
  } catch {
@@ -6027,13 +6937,13 @@ var init_signal = __esm({
6027
6937
  // bin/jpi-learn.js
6028
6938
  var jpi_learn_exports = {};
6029
6939
  __export(jpi_learn_exports, {
6030
- run: () => run7
6940
+ run: () => run10
6031
6941
  });
6032
- async function run7() {
6942
+ async function run10() {
6033
6943
  try {
6034
- const args3 = process.argv.slice(2);
6035
- const cwdIdx = args3.indexOf("--cwd");
6036
- const cwd = cwdIdx !== -1 && args3[cwdIdx + 1] ? args3[cwdIdx + 1] : process.cwd();
6944
+ const args5 = process.argv.slice(2);
6945
+ const cwdIdx = args5.indexOf("--cwd");
6946
+ const cwd = cwdIdx !== -1 && args5[cwdIdx + 1] ? args5[cwdIdx + 1] : process.cwd();
6037
6947
  const { extractFingerprint: extractFingerprint2 } = await Promise.resolve().then(() => (init_signal(), signal_exports));
6038
6948
  const { readProfile: readProfile2, writeProfile: writeProfile2, accumulateSession: accumulateSession2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
6039
6949
  const fingerprint = extractFingerprint2(cwd);
@@ -6056,7 +6966,7 @@ var init_jpi_learn = __esm({
6056
6966
  "use strict";
6057
6967
  isMain = process.argv[1]?.endsWith("jpi-learn.js") || process.argv[1]?.endsWith("jpi-learn");
6058
6968
  if (isMain) {
6059
- run7();
6969
+ run10();
6060
6970
  }
6061
6971
  }
6062
6972
  });
@@ -6064,23 +6974,23 @@ var init_jpi_learn = __esm({
6064
6974
  // bin/jpi-config.js
6065
6975
  var jpi_config_exports = {};
6066
6976
  __export(jpi_config_exports, {
6067
- run: () => run8
6977
+ run: () => run11
6068
6978
  });
6069
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, existsSync as existsSync7 } from "fs";
6070
- import { join as join10 } from "path";
6071
- import { homedir as homedir8 } from "os";
6979
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, existsSync as existsSync7 } from "fs";
6980
+ import { join as join12 } from "path";
6981
+ import { homedir as homedir10 } from "os";
6072
6982
  function readConfig() {
6073
6983
  try {
6074
6984
  if (!existsSync7(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
6075
- return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync10(CONFIG_FILE, "utf8")) };
6985
+ return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync12(CONFIG_FILE, "utf8")) };
6076
6986
  } catch {
6077
6987
  return { ...DEFAULT_CONFIG };
6078
6988
  }
6079
6989
  }
6080
6990
  function writeConfig(patch) {
6081
- mkdirSync7(TERMINALHIRE_DIR7, { recursive: true });
6991
+ mkdirSync9(TERMINALHIRE_DIR9, { recursive: true });
6082
6992
  const merged = { ...readConfig(), ...patch };
6083
- writeFileSync7(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
6993
+ writeFileSync9(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
6084
6994
  }
6085
6995
  function parseNudgeMode(raw) {
6086
6996
  if (raw === "session" || raw === "always") return raw;
@@ -6088,9 +6998,9 @@ function parseNudgeMode(raw) {
6088
6998
  if (m && parseInt(m[1], 10) >= 1) return raw;
6089
6999
  return null;
6090
7000
  }
6091
- async function run8() {
6092
- const args3 = process.argv.slice(2);
6093
- const filtered = args3[0] === "config" ? args3.slice(1) : args3;
7001
+ async function run11() {
7002
+ const args5 = process.argv.slice(2);
7003
+ const filtered = args5[0] === "config" ? args5.slice(1) : args5;
6094
7004
  if (filtered.includes("--show") || filtered.length === 0) {
6095
7005
  const cfg = readConfig();
6096
7006
  const envOverride = process.env["TERMINALHIRE_NUDGE"];
@@ -6131,12 +7041,12 @@ async function run8() {
6131
7041
  console.error(" terminalhire config --show");
6132
7042
  process.exit(1);
6133
7043
  }
6134
- var TERMINALHIRE_DIR7, CONFIG_FILE, DEFAULT_CONFIG;
7044
+ var TERMINALHIRE_DIR9, CONFIG_FILE, DEFAULT_CONFIG;
6135
7045
  var init_jpi_config = __esm({
6136
7046
  "bin/jpi-config.js"() {
6137
7047
  "use strict";
6138
- TERMINALHIRE_DIR7 = join10(homedir8(), ".terminalhire");
6139
- CONFIG_FILE = join10(TERMINALHIRE_DIR7, "config.json");
7048
+ TERMINALHIRE_DIR9 = join12(homedir10(), ".terminalhire");
7049
+ CONFIG_FILE = join12(TERMINALHIRE_DIR9, "config.json");
6140
7050
  DEFAULT_CONFIG = { nudge: "session" };
6141
7051
  }
6142
7052
  });
@@ -6159,25 +7069,25 @@ __export(spinner_exports, {
6159
7069
  readSpinnerConfig: () => readSpinnerConfig
6160
7070
  });
6161
7071
  import {
6162
- readFileSync as readFileSync11,
6163
- writeFileSync as writeFileSync8,
7072
+ readFileSync as readFileSync13,
7073
+ writeFileSync as writeFileSync10,
6164
7074
  existsSync as existsSync8,
6165
- mkdirSync as mkdirSync8,
7075
+ mkdirSync as mkdirSync10,
6166
7076
  renameSync as renameSync2
6167
7077
  } from "fs";
6168
- import { join as join11, dirname } from "path";
6169
- import { homedir as homedir9 } from "os";
7078
+ import { join as join13, dirname } from "path";
7079
+ import { homedir as homedir11 } from "os";
6170
7080
  function readJson(path, fallback) {
6171
7081
  try {
6172
- return existsSync8(path) ? JSON.parse(readFileSync11(path, "utf8")) : fallback;
7082
+ return existsSync8(path) ? JSON.parse(readFileSync13(path, "utf8")) : fallback;
6173
7083
  } catch {
6174
7084
  return fallback;
6175
7085
  }
6176
7086
  }
6177
7087
  function atomicWriteJson(path, obj) {
6178
- mkdirSync8(dirname(path), { recursive: true });
7088
+ mkdirSync10(dirname(path), { recursive: true });
6179
7089
  const tmp = `${path}.tmp-${process.pid}`;
6180
- writeFileSync8(tmp, JSON.stringify(obj, null, 2) + "\n", "utf8");
7090
+ writeFileSync10(tmp, JSON.stringify(obj, null, 2) + "\n", "utf8");
6181
7091
  renameSync2(tmp, path);
6182
7092
  }
6183
7093
  function titleCase(s) {
@@ -6429,10 +7339,10 @@ var TH_DIR, CLAUDE_SETTINGS, CONFIG_FILE2, SPINNER_STATE_FILE, SPINNER_DEFAULTS,
6429
7339
  var init_spinner = __esm({
6430
7340
  "bin/spinner.js"() {
6431
7341
  "use strict";
6432
- TH_DIR = process.env["TERMINALHIRE_DIR"] || join11(homedir9(), ".terminalhire");
6433
- CLAUDE_SETTINGS = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join11(homedir9(), ".claude", "settings.json");
6434
- CONFIG_FILE2 = join11(TH_DIR, "config.json");
6435
- SPINNER_STATE_FILE = join11(TH_DIR, "spinner-state.json");
7342
+ TH_DIR = process.env["TERMINALHIRE_DIR"] || join13(homedir11(), ".terminalhire");
7343
+ CLAUDE_SETTINGS = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join13(homedir11(), ".claude", "settings.json");
7344
+ CONFIG_FILE2 = join13(TH_DIR, "config.json");
7345
+ SPINNER_STATE_FILE = join13(TH_DIR, "spinner-state.json");
6436
7346
  SPINNER_DEFAULTS = { enabled: false, mode: "append", max: 6, frequency: "sometimes" };
6437
7347
  VERB_INTROS = ["Matched:", "You\u2019d fit:", "Worth a look:", "On your radar:", "Fits your stack:"];
6438
7348
  }
@@ -6441,29 +7351,29 @@ var init_spinner = __esm({
6441
7351
  // bin/jpi-spinner.js
6442
7352
  var jpi_spinner_exports = {};
6443
7353
  __export(jpi_spinner_exports, {
6444
- run: () => run9
7354
+ run: () => run12
6445
7355
  });
6446
7356
  import {
6447
- readFileSync as readFileSync12,
6448
- writeFileSync as writeFileSync9,
7357
+ readFileSync as readFileSync14,
7358
+ writeFileSync as writeFileSync11,
6449
7359
  copyFileSync,
6450
7360
  existsSync as existsSync9,
6451
- mkdirSync as mkdirSync9
7361
+ mkdirSync as mkdirSync11
6452
7362
  } from "fs";
6453
- import { join as join12 } from "path";
6454
- import { homedir as homedir10 } from "os";
6455
- import { createInterface as createInterface5 } from "readline";
7363
+ import { join as join14 } from "path";
7364
+ import { homedir as homedir12 } from "os";
7365
+ import { createInterface as createInterface7 } from "readline";
6456
7366
  function readConfig2() {
6457
7367
  try {
6458
- return existsSync9(CONFIG_FILE3) ? JSON.parse(readFileSync12(CONFIG_FILE3, "utf8")) : {};
7368
+ return existsSync9(CONFIG_FILE3) ? JSON.parse(readFileSync14(CONFIG_FILE3, "utf8")) : {};
6459
7369
  } catch {
6460
7370
  return {};
6461
7371
  }
6462
7372
  }
6463
7373
  function writeConfig2(patch) {
6464
- mkdirSync9(TH_DIR2, { recursive: true });
7374
+ mkdirSync11(TH_DIR2, { recursive: true });
6465
7375
  const merged = { ...readConfig2(), ...patch };
6466
- writeFileSync9(CONFIG_FILE3, JSON.stringify(merged, null, 2) + "\n", "utf8");
7376
+ writeFileSync11(CONFIG_FILE3, JSON.stringify(merged, null, 2) + "\n", "utf8");
6467
7377
  }
6468
7378
  function backupSettings() {
6469
7379
  if (!existsSync9(SETTINGS_PATH)) return null;
@@ -6473,7 +7383,7 @@ function backupSettings() {
6473
7383
  return backupPath;
6474
7384
  }
6475
7385
  function ask(question) {
6476
- const rl = createInterface5({ input: process.stdin, output: process.stdout });
7386
+ const rl = createInterface7({ input: process.stdin, output: process.stdout });
6477
7387
  return new Promise((res) => {
6478
7388
  rl.question(question, (answer) => {
6479
7389
  rl.close();
@@ -6483,20 +7393,20 @@ function ask(question) {
6483
7393
  }
6484
7394
  function readTopMatches() {
6485
7395
  try {
6486
- const c = JSON.parse(readFileSync12(CACHE_FILE, "utf8"));
7396
+ const c = JSON.parse(readFileSync14(CACHE_FILE, "utf8"));
6487
7397
  return Array.isArray(c.topMatches) ? c.topMatches : [];
6488
7398
  } catch {
6489
7399
  return [];
6490
7400
  }
6491
7401
  }
6492
- async function run9() {
6493
- const args3 = process.argv.slice(2).filter((a) => a !== "spinner");
6494
- const has = (f) => args3.includes(f);
7402
+ async function run12() {
7403
+ const args5 = process.argv.slice(2).filter((a) => a !== "spinner");
7404
+ const has = (f) => args5.includes(f);
6495
7405
  const val = (f) => {
6496
- const i = args3.indexOf(f);
6497
- return i >= 0 ? args3[i + 1] : void 0;
7406
+ const i = args5.indexOf(f);
7407
+ return i >= 0 ? args5[i + 1] : void 0;
6498
7408
  };
6499
- if (has("--show") || args3.length === 0) {
7409
+ if (has("--show") || args5.length === 0) {
6500
7410
  const sc = readSpinnerConfig();
6501
7411
  console.log("");
6502
7412
  console.log("terminalhire spinner \u2014 job matches in the Claude Code spinner line");
@@ -6626,24 +7536,24 @@ var init_jpi_spinner = __esm({
6626
7536
  "bin/jpi-spinner.js"() {
6627
7537
  "use strict";
6628
7538
  init_spinner();
6629
- TH_DIR2 = process.env["TERMINALHIRE_DIR"] || join12(homedir10(), ".terminalhire");
6630
- CONFIG_FILE3 = join12(TH_DIR2, "config.json");
6631
- SETTINGS_PATH = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join12(homedir10(), ".claude", "settings.json");
6632
- CACHE_FILE = join12(TH_DIR2, "index-cache.json");
7539
+ TH_DIR2 = process.env["TERMINALHIRE_DIR"] || join14(homedir12(), ".terminalhire");
7540
+ CONFIG_FILE3 = join14(TH_DIR2, "config.json");
7541
+ SETTINGS_PATH = process.env["TERMINALHIRE_CLAUDE_SETTINGS"] || join14(homedir12(), ".claude", "settings.json");
7542
+ CACHE_FILE = join14(TH_DIR2, "index-cache.json");
6633
7543
  }
6634
7544
  });
6635
7545
 
6636
7546
  // bin/jpi-sync.js
6637
7547
  var jpi_sync_exports = {};
6638
7548
  __export(jpi_sync_exports, {
6639
- run: () => run10
7549
+ run: () => run13
6640
7550
  });
6641
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, existsSync as existsSync10, rmSync as rmSync2 } from "fs";
6642
- import { join as join13 } from "path";
6643
- import { homedir as homedir11, hostname as osHostname } from "os";
6644
- import { createInterface as createInterface6 } from "readline";
7551
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync12, mkdirSync as mkdirSync12, existsSync as existsSync10, rmSync as rmSync2 } from "fs";
7552
+ import { join as join15 } from "path";
7553
+ import { homedir as homedir13, hostname as osHostname } from "os";
7554
+ import { createInterface as createInterface8 } from "readline";
6645
7555
  function ask2(question) {
6646
- const rl = createInterface6({ input: process.stdin, output: process.stdout });
7556
+ const rl = createInterface8({ input: process.stdin, output: process.stdout });
6647
7557
  return new Promise((res) => {
6648
7558
  rl.question(question, (answer) => {
6649
7559
  rl.close();
@@ -6653,14 +7563,14 @@ function ask2(question) {
6653
7563
  }
6654
7564
  function readMarker() {
6655
7565
  try {
6656
- return existsSync10(TIER1_MARKER) ? JSON.parse(readFileSync13(TIER1_MARKER, "utf8")) : null;
7566
+ return existsSync10(TIER1_MARKER) ? JSON.parse(readFileSync15(TIER1_MARKER, "utf8")) : null;
6657
7567
  } catch {
6658
7568
  return null;
6659
7569
  }
6660
7570
  }
6661
7571
  function writeMarker(marker) {
6662
- mkdirSync10(TH_DIR3, { recursive: true });
6663
- writeFileSync10(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
7572
+ mkdirSync12(TH_DIR3, { recursive: true });
7573
+ writeFileSync12(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
6664
7574
  }
6665
7575
  function clearMarker() {
6666
7576
  try {
@@ -6726,7 +7636,7 @@ async function runPush() {
6726
7636
  const fields = buildConsentFields(profile);
6727
7637
  renderPreview(fields);
6728
7638
  await new Promise((resolve2) => {
6729
- const rl = createInterface6({ input: process.stdin, output: process.stdout });
7639
+ const rl = createInterface8({ input: process.stdin, output: process.stdout });
6730
7640
  rl.question(
6731
7641
  " Press Enter to open your browser to authorize + consent (or Ctrl-C to cancel)... ",
6732
7642
  () => {
@@ -6924,7 +7834,7 @@ async function runDelete() {
6924
7834
  console.log("\n Requesting deletion...");
6925
7835
  let res;
6926
7836
  try {
6927
- res = await fetch(`${API_URL3}/api/profile-sync`, {
7837
+ res = await fetch(`${API_URL4}/api/profile-sync`, {
6928
7838
  method: "DELETE",
6929
7839
  headers: { "Content-Type": "application/json" },
6930
7840
  body: JSON.stringify({ consentToken, login, deleteToken }),
@@ -6948,9 +7858,9 @@ async function runDelete() {
6948
7858
  clearMarker();
6949
7859
  console.log("\n Synced profile deleted and local marker cleared.\n");
6950
7860
  }
6951
- async function run10() {
6952
- const args3 = process.argv.slice(2).filter((a) => a !== "sync");
6953
- const has = (f) => args3.includes(f);
7861
+ async function run13() {
7862
+ const args5 = process.argv.slice(2).filter((a) => a !== "sync");
7863
+ const has = (f) => args5.includes(f);
6954
7864
  if (has("--push") || has("--enable")) {
6955
7865
  await runPush();
6956
7866
  return;
@@ -6974,14 +7884,14 @@ async function run10() {
6974
7884
  console.log(" This is NOT required to use terminalhire.");
6975
7885
  console.log("");
6976
7886
  }
6977
- var TH_DIR3, TIER1_MARKER, API_URL3, SYNC_BASE, POLL_INTERVAL_MS, POLL_TIMEOUT_MS, CONSENT_VERSION;
7887
+ var TH_DIR3, TIER1_MARKER, API_URL4, SYNC_BASE, POLL_INTERVAL_MS, POLL_TIMEOUT_MS, CONSENT_VERSION;
6978
7888
  var init_jpi_sync = __esm({
6979
7889
  "bin/jpi-sync.js"() {
6980
7890
  "use strict";
6981
7891
  init_open_url();
6982
- TH_DIR3 = process.env["TERMINALHIRE_DIR"] || join13(homedir11(), ".terminalhire");
6983
- TIER1_MARKER = join13(TH_DIR3, "tier1.json");
6984
- API_URL3 = process.env["TERMINALHIRE_API_URL"] || process.env["JPI_API_URL"] || "https://terminalhire.com";
7892
+ TH_DIR3 = process.env["TERMINALHIRE_DIR"] || join15(homedir13(), ".terminalhire");
7893
+ TIER1_MARKER = join15(TH_DIR3, "tier1.json");
7894
+ API_URL4 = process.env["TERMINALHIRE_API_URL"] || process.env["JPI_API_URL"] || "https://terminalhire.com";
6985
7895
  SYNC_BASE = "https://www.terminalhire.com";
6986
7896
  POLL_INTERVAL_MS = 2e3;
6987
7897
  POLL_TIMEOUT_MS = 10 * 60 * 1e3;
@@ -6992,16 +7902,16 @@ var init_jpi_sync = __esm({
6992
7902
  // bin/jpi-init.js
6993
7903
  var jpi_init_exports = {};
6994
7904
  __export(jpi_init_exports, {
6995
- run: () => run11
7905
+ run: () => run14
6996
7906
  });
6997
7907
  import { existsSync as existsSync11 } from "fs";
6998
- import { join as join14, resolve } from "path";
7908
+ import { join as join16, resolve } from "path";
6999
7909
  import { fileURLToPath as fileURLToPath3 } from "url";
7000
- import { createInterface as createInterface7 } from "readline";
7910
+ import { createInterface as createInterface9 } from "readline";
7001
7911
  import { spawnSync, spawn as spawn2 } from "child_process";
7002
- import { homedir as homedir12 } from "os";
7912
+ import { homedir as homedir14 } from "os";
7003
7913
  function ask3(question) {
7004
- const rl = createInterface7({ input: process.stdin, output: process.stdout });
7914
+ const rl = createInterface9({ input: process.stdin, output: process.stdout });
7005
7915
  return new Promise((resolve2) => {
7006
7916
  rl.question(question, (answer) => {
7007
7917
  rl.close();
@@ -7010,18 +7920,18 @@ function ask3(question) {
7010
7920
  });
7011
7921
  }
7012
7922
  function resolveScript(name) {
7013
- const distPath = resolve(join14(__dirname2, "..", "..", "dist", "bin", `${name}.js`));
7014
- const legacyPath = resolve(join14(__dirname2, `${name}.js`));
7923
+ const distPath = resolve(join16(__dirname2, "..", "..", "dist", "bin", `${name}.js`));
7924
+ const legacyPath = resolve(join16(__dirname2, `${name}.js`));
7015
7925
  return existsSync11(distPath) ? distPath : legacyPath;
7016
7926
  }
7017
7927
  function resolveInstallJs() {
7018
- const fromDist = resolve(join14(__dirname2, "..", "..", "install.js"));
7019
- const fromBin = resolve(join14(__dirname2, "..", "install.js"));
7928
+ const fromDist = resolve(join16(__dirname2, "..", "..", "install.js"));
7929
+ const fromBin = resolve(join16(__dirname2, "..", "install.js"));
7020
7930
  if (existsSync11(fromDist)) return fromDist;
7021
7931
  if (existsSync11(fromBin)) return fromBin;
7022
7932
  return fromBin;
7023
7933
  }
7024
- async function run11() {
7934
+ async function run14() {
7025
7935
  console.log("");
7026
7936
  console.log("\u250C\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\u2510");
7027
7937
  console.log("\u2502 terminalhire init \u2014 one-command onboarding \u2502");
@@ -7124,17 +8034,17 @@ var init_jpi_init = __esm({
7124
8034
  // bin/jpi-refresh.js
7125
8035
  var jpi_refresh_exports = {};
7126
8036
  __export(jpi_refresh_exports, {
7127
- run: () => run12
8037
+ run: () => run15
7128
8038
  });
7129
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync11, existsSync as existsSync12, mkdirSync as mkdirSync11 } from "fs";
7130
- import { join as join15 } from "path";
7131
- import { homedir as homedir13 } from "os";
8039
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync13, existsSync as existsSync12, mkdirSync as mkdirSync13 } from "fs";
8040
+ import { join as join17 } from "path";
8041
+ import { homedir as homedir15 } from "os";
7132
8042
  import { fileURLToPath as fileURLToPath4 } from "url";
7133
- async function run12() {
8043
+ async function run15() {
7134
8044
  try {
7135
8045
  let index;
7136
8046
  try {
7137
- const res = await fetch(`${API_URL4}/api/index`, {
8047
+ const res = await fetch(`${API_URL5}/api/index`, {
7138
8048
  signal: AbortSignal.timeout(15e3),
7139
8049
  headers: { "Accept": "application/json" }
7140
8050
  });
@@ -7182,14 +8092,14 @@ async function run12() {
7182
8092
  }
7183
8093
  } catch {
7184
8094
  }
7185
- mkdirSync11(TERMINALHIRE_DIR8, { recursive: true });
8095
+ mkdirSync13(TERMINALHIRE_DIR10, { recursive: true });
7186
8096
  const cacheEntry = {
7187
8097
  ts: Date.now(),
7188
8098
  index,
7189
8099
  matchCount,
7190
8100
  topMatches
7191
8101
  };
7192
- writeFileSync11(INDEX_CACHE_FILE4, JSON.stringify(cacheEntry), "utf8");
8102
+ writeFileSync13(INDEX_CACHE_FILE4, JSON.stringify(cacheEntry), "utf8");
7193
8103
  try {
7194
8104
  const {
7195
8105
  readSpinnerConfig: readSpinnerConfig2,
@@ -7216,7 +8126,7 @@ async function run12() {
7216
8126
  const verbs = buildSpinnerPool2(ranked, sc.max, { sessionTags, frequency: sc.frequency });
7217
8127
  if (verbs.length > 0) applySpinnerVerbs2(verbs, sc.mode);
7218
8128
  else clearSpinnerVerbs2();
7219
- const tips = buildTips2(ranked, API_URL4, 8);
8129
+ const tips = buildTips2(ranked, API_URL5, 8);
7220
8130
  if (tips.length > 0) applySpinnerTips2(tips);
7221
8131
  else clearSpinnerTips2();
7222
8132
  } else {
@@ -7233,30 +8143,30 @@ async function run12() {
7233
8143
  process.exit(1);
7234
8144
  }
7235
8145
  }
7236
- var __dirname3, TERMINALHIRE_DIR8, INDEX_CACHE_FILE4, API_URL4;
8146
+ var __dirname3, TERMINALHIRE_DIR10, INDEX_CACHE_FILE4, API_URL5;
7237
8147
  var init_jpi_refresh = __esm({
7238
8148
  "bin/jpi-refresh.js"() {
7239
8149
  "use strict";
7240
8150
  __dirname3 = fileURLToPath4(new URL(".", import.meta.url));
7241
- TERMINALHIRE_DIR8 = join15(homedir13(), ".terminalhire");
7242
- INDEX_CACHE_FILE4 = join15(TERMINALHIRE_DIR8, "index-cache.json");
7243
- API_URL4 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
8151
+ TERMINALHIRE_DIR10 = join17(homedir15(), ".terminalhire");
8152
+ INDEX_CACHE_FILE4 = join17(TERMINALHIRE_DIR10, "index-cache.json");
8153
+ API_URL5 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
7244
8154
  }
7245
8155
  });
7246
8156
 
7247
8157
  // bin/jpi-save.js
7248
8158
  var jpi_save_exports = {};
7249
8159
  __export(jpi_save_exports, {
7250
- run: () => run13
8160
+ run: () => run16
7251
8161
  });
7252
- import { readFileSync as readFileSync15, existsSync as existsSync13 } from "fs";
7253
- import { join as join16 } from "path";
7254
- import { homedir as homedir14 } from "os";
8162
+ import { readFileSync as readFileSync17, existsSync as existsSync13 } from "fs";
8163
+ import { join as join18 } from "path";
8164
+ import { homedir as homedir16 } from "os";
7255
8165
  import { fileURLToPath as fileURLToPath5 } from "url";
7256
8166
  function findJobInCache(jobId) {
7257
8167
  try {
7258
8168
  if (!existsSync13(INDEX_CACHE_FILE5)) return null;
7259
- const raw = readFileSync15(INDEX_CACHE_FILE5, "utf8");
8169
+ const raw = readFileSync17(INDEX_CACHE_FILE5, "utf8");
7260
8170
  const entry = JSON.parse(raw);
7261
8171
  const jobs = entry?.index?.jobs ?? [];
7262
8172
  return jobs.find((j) => j.id === jobId) ?? null;
@@ -7325,7 +8235,7 @@ async function cmdUnsave(jobId) {
7325
8235
  process.exit(1);
7326
8236
  }
7327
8237
  }
7328
- async function run13() {
8238
+ async function run16() {
7329
8239
  const verb = process.argv[2];
7330
8240
  const jobId = process.argv[3];
7331
8241
  try {
@@ -7344,31 +8254,31 @@ async function run13() {
7344
8254
  process.exit(1);
7345
8255
  }
7346
8256
  }
7347
- var __dirname4, TERMINALHIRE_DIR9, INDEX_CACHE_FILE5;
8257
+ var __dirname4, TERMINALHIRE_DIR11, INDEX_CACHE_FILE5;
7348
8258
  var init_jpi_save = __esm({
7349
8259
  "bin/jpi-save.js"() {
7350
8260
  "use strict";
7351
8261
  __dirname4 = fileURLToPath5(new URL(".", import.meta.url));
7352
- TERMINALHIRE_DIR9 = join16(homedir14(), ".terminalhire");
7353
- INDEX_CACHE_FILE5 = join16(TERMINALHIRE_DIR9, "index-cache.json");
8262
+ TERMINALHIRE_DIR11 = join18(homedir16(), ".terminalhire");
8263
+ INDEX_CACHE_FILE5 = join18(TERMINALHIRE_DIR11, "index-cache.json");
7354
8264
  }
7355
8265
  });
7356
8266
 
7357
8267
  // bin/jpi-dispatch.js
7358
8268
  import { fileURLToPath as fileURLToPath6 } from "url";
7359
- import { join as join17, dirname as dirname2 } from "path";
7360
- import { existsSync as existsSync14, readFileSync as readFileSync16 } from "fs";
8269
+ import { join as join19, dirname as dirname2 } from "path";
8270
+ import { existsSync as existsSync14, readFileSync as readFileSync18 } from "fs";
7361
8271
  import { createRequire } from "module";
7362
8272
  var __dirname5 = fileURLToPath6(new URL(".", import.meta.url));
7363
8273
  function readPackageVersion() {
7364
8274
  try {
7365
8275
  const candidates = [
7366
- join17(__dirname5, "..", "..", "package.json"),
7367
- join17(__dirname5, "..", "package.json")
8276
+ join19(__dirname5, "..", "..", "package.json"),
8277
+ join19(__dirname5, "..", "package.json")
7368
8278
  ];
7369
8279
  for (const p of candidates) {
7370
8280
  if (existsSync14(p)) {
7371
- const pkg = JSON.parse(readFileSync16(p, "utf8"));
8281
+ const pkg = JSON.parse(readFileSync18(p, "utf8"));
7372
8282
  if (pkg.version) return pkg.version;
7373
8283
  }
7374
8284
  }
@@ -7379,7 +8289,7 @@ function readPackageVersion() {
7379
8289
  var firstArg = process.argv[2];
7380
8290
  if (!firstArg && !process.stdin.isTTY) {
7381
8291
  const { default: childProcess } = await import("child_process");
7382
- const nudgeScript = join17(__dirname5, "jpi.js");
8292
+ const nudgeScript = join19(__dirname5, "jpi.js");
7383
8293
  const child = childProcess.spawnSync(process.execPath, [nudgeScript], {
7384
8294
  stdio: ["inherit", "inherit", "inherit"]
7385
8295
  });
@@ -7396,6 +8306,10 @@ if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-
7396
8306
  console.log(" terminalhire jobs Fetch job index, match locally, browse roles");
7397
8307
  console.log(" terminalhire jobs --limit N Show top N results (default: 10)");
7398
8308
  console.log(" terminalhire jobs --remote-only Filter to remote roles only");
8309
+ console.log(" terminalhire devs Rank opted-in builders & projects locally (dev\u2194dev)");
8310
+ console.log(" terminalhire devs --as-project Rank builders against your declared project (founder side)");
8311
+ console.log(' terminalhire project "<title>: <skills>" Declare a project locally (never sent) to rank builders for');
8312
+ console.log(" terminalhire project --show Show your locally-declared project");
7399
8313
  console.log(" terminalhire bounties Day-sized paid tasks you can knock out today");
7400
8314
  console.log(" terminalhire bounties --priced Only bounties with a known $ amount");
7401
8315
  console.log(" terminalhire claim record <id|issueUrl> Claim a bounty locally + print the executor brief");
@@ -7406,6 +8320,7 @@ if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-
7406
8320
  console.log(" terminalhire trajectory --inward Also show private rework/recovery (never exported)");
7407
8321
  console.log(" terminalhire trajectory --push Opt-in: link your derived trajectory to your dashboard (typed-yes)");
7408
8322
  console.log(" terminalhire trajectory --push --delete Unlink (revoke) your trajectory from the dashboard");
8323
+ console.log(" terminalhire intro <github-login> Request a consented intro to another developer (typed-yes)");
7409
8324
  console.log(" terminalhire profile --show Display your encrypted local profile");
7410
8325
  console.log(" terminalhire profile --edit Set displayName, contactEmail, prefs");
7411
8326
  console.log(" terminalhire profile --delete Wipe profile and encryption key from disk");
@@ -7452,6 +8367,18 @@ if (firstArg === "jobs") {
7452
8367
  await mod.run();
7453
8368
  process.exit(0);
7454
8369
  }
8370
+ if (firstArg === "devs") {
8371
+ process.argv.splice(2, 1);
8372
+ const mod = await Promise.resolve().then(() => (init_jpi_devs(), jpi_devs_exports));
8373
+ await mod.run();
8374
+ process.exit(0);
8375
+ }
8376
+ if (firstArg === "project") {
8377
+ process.argv.splice(2, 1);
8378
+ const mod = await Promise.resolve().then(() => (init_jpi_project(), jpi_project_exports));
8379
+ await mod.run();
8380
+ process.exit(0);
8381
+ }
7455
8382
  if (firstArg === "bounties") {
7456
8383
  process.argv.splice(2, 1);
7457
8384
  const mod = await Promise.resolve().then(() => (init_jpi_bounties(), jpi_bounties_exports));
@@ -7470,6 +8397,12 @@ if (firstArg === "trajectory" || firstArg === "mirror") {
7470
8397
  await mod.run();
7471
8398
  process.exit(0);
7472
8399
  }
8400
+ if (firstArg === "intro") {
8401
+ process.argv.splice(2, 1);
8402
+ const mod = await Promise.resolve().then(() => (init_jpi_intro(), jpi_intro_exports));
8403
+ await mod.run();
8404
+ process.exit(0);
8405
+ }
7473
8406
  if (firstArg === "profile") {
7474
8407
  const mod = await Promise.resolve().then(() => (init_jpi_profile(), jpi_profile_exports));
7475
8408
  await mod.run();