terminalhire 0.2.0 → 0.2.3

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.
@@ -1461,13 +1461,13 @@ async function removeSavedJob(id) {
1461
1461
  return true;
1462
1462
  }
1463
1463
  async function deleteProfile() {
1464
- const { rmSync: rmSync2 } = await import("fs");
1464
+ const { rmSync: rmSync3 } = await import("fs");
1465
1465
  try {
1466
- rmSync2(PROFILE_FILE);
1466
+ rmSync3(PROFILE_FILE);
1467
1467
  } catch {
1468
1468
  }
1469
1469
  try {
1470
- rmSync2(KEY_FILE2);
1470
+ rmSync3(KEY_FILE2);
1471
1471
  } catch {
1472
1472
  }
1473
1473
  }
@@ -1553,11 +1553,11 @@ async function runLogin() {
1553
1553
  if (process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["JPI_GITHUB_MOCK"] === "1") {
1554
1554
  const { createRequire: createRequire2 } = await import("module");
1555
1555
  const { fileURLToPath: fileURLToPath7 } = await import("url");
1556
- const { join: join13, dirname: dirname3 } = await import("path");
1556
+ const { join: join14, dirname: dirname3 } = await import("path");
1557
1557
  const __dirname6 = fileURLToPath7(new URL(".", import.meta.url));
1558
- const fixturePath = join13(__dirname6, "../../fixtures/github-sample.json");
1559
- const { readFileSync: readFileSync12 } = await import("fs");
1560
- ghProfile = JSON.parse(readFileSync12(fixturePath, "utf8"));
1558
+ const fixturePath = join14(__dirname6, "../../fixtures/github-sample.json");
1559
+ const { readFileSync: readFileSync13 } = await import("fs");
1560
+ ghProfile = JSON.parse(readFileSync13(fixturePath, "utf8"));
1561
1561
  } else {
1562
1562
  ghProfile = await fetchGitHubProfile2(login, token);
1563
1563
  }
@@ -2474,10 +2474,12 @@ function clearSpinnerVerbs() {
2474
2474
  }
2475
2475
  return { cleared: true, keptUserVerbs };
2476
2476
  }
2477
- function buildTips(topMatches, baseUrl, max = 3) {
2477
+ function buildTips(topMatches, baseUrl, max = 8) {
2478
2478
  const base = String(baseUrl || "https://terminalhire.com").replace(/\/+$/, "");
2479
2479
  const out = [];
2480
- const seen = /* @__PURE__ */ new Set();
2480
+ const seenRole = /* @__PURE__ */ new Set();
2481
+ const perCompany = /* @__PURE__ */ new Map();
2482
+ const COMPANY_CAP = 2;
2481
2483
  for (const m of Array.isArray(topMatches) ? topMatches : []) {
2482
2484
  if (!m || !m.title || !m.company || !m.id) continue;
2483
2485
  const idx = String(m.id).indexOf(":");
@@ -2485,14 +2487,20 @@ function buildTips(topMatches, baseUrl, max = 3) {
2485
2487
  const source = String(m.id).slice(0, idx);
2486
2488
  const ext = String(m.id).slice(idx + 1);
2487
2489
  if (!source || !ext) continue;
2488
- const key = `${source}/${ext}`;
2489
- if (seen.has(key)) continue;
2490
- seen.add(key);
2491
- let title = String(m.title).trim().replace(/\s+/g, " ");
2490
+ const companyRaw = String(m.company).trim().replace(/\s+/g, " ");
2491
+ const titleRaw = String(m.title).trim().replace(/\s+/g, " ");
2492
+ const roleKey = `${titleRaw.toLowerCase()}@${companyRaw.toLowerCase()}`;
2493
+ const coKey = companyRaw.toLowerCase();
2494
+ if (seenRole.has(roleKey)) continue;
2495
+ if ((perCompany.get(coKey) || 0) >= COMPANY_CAP) continue;
2496
+ seenRole.add(roleKey);
2497
+ perCompany.set(coKey, (perCompany.get(coKey) || 0) + 1);
2498
+ let title = titleRaw;
2492
2499
  if (title.length > 34) title = title.slice(0, 33).trimEnd() + "\u2026";
2493
- const company = titleCase(String(m.company).trim().replace(/\s+/g, " "));
2500
+ const company = titleCase(companyRaw);
2494
2501
  const pct = Math.max(1, Math.min(99, Math.round((Number(m.score) || 0) * 100)));
2495
- const url = `${base}/j/${source}/${encodeURIComponent(ext)}`;
2502
+ const token = Buffer.from(String(m.id)).toString("base64url");
2503
+ const url = `${base}/j/${token}`;
2496
2504
  out.push(`\u2197 ${title} @ ${company} \xB7 ${pct}% \u2014 ${url}`);
2497
2505
  if (out.length >= max) break;
2498
2506
  }
@@ -2743,19 +2751,292 @@ var init_jpi_spinner = __esm({
2743
2751
  }
2744
2752
  });
2745
2753
 
2754
+ // bin/jpi-sync.js
2755
+ var jpi_sync_exports = {};
2756
+ __export(jpi_sync_exports, {
2757
+ run: () => run7
2758
+ });
2759
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, existsSync as existsSync7, rmSync as rmSync2 } from "fs";
2760
+ import { join as join9 } from "path";
2761
+ import { homedir as homedir7 } from "os";
2762
+ import { createInterface as createInterface4 } from "readline";
2763
+ function ask2(question) {
2764
+ const rl = createInterface4({ input: process.stdin, output: process.stdout });
2765
+ return new Promise((res) => {
2766
+ rl.question(question, (answer) => {
2767
+ rl.close();
2768
+ res(answer.trim().toLowerCase());
2769
+ });
2770
+ });
2771
+ }
2772
+ function readMarker() {
2773
+ try {
2774
+ return existsSync7(TIER1_MARKER) ? JSON.parse(readFileSync9(TIER1_MARKER, "utf8")) : null;
2775
+ } catch {
2776
+ return null;
2777
+ }
2778
+ }
2779
+ function writeMarker(marker) {
2780
+ mkdirSync7(TH_DIR3, { recursive: true });
2781
+ writeFileSync7(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
2782
+ }
2783
+ function clearMarker() {
2784
+ try {
2785
+ rmSync2(TIER1_MARKER);
2786
+ } catch {
2787
+ }
2788
+ }
2789
+ function buildConsentFields(profile) {
2790
+ const gh = profile.github || {};
2791
+ const fields = [
2792
+ { key: "login", label: "GitHub login", value: gh.login },
2793
+ { key: "publicEmail", label: "Public email", value: gh.publicEmail || null },
2794
+ { key: "topLanguages", label: "Top languages", value: gh.topLanguages || [] },
2795
+ { key: "skillTags", label: "Skill tags", value: profile.skillTags || [] }
2796
+ ];
2797
+ if (profile.displayName) {
2798
+ fields.push({ key: "displayName", label: "Display name", value: profile.displayName });
2799
+ }
2800
+ if (profile.contactEmail) {
2801
+ fields.push({ key: "contactEmail", label: "Contact email", value: profile.contactEmail });
2802
+ }
2803
+ return fields;
2804
+ }
2805
+ function renderConsentCard(fields) {
2806
+ console.log("");
2807
+ 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\u2510");
2808
+ console.log("\u2502 terminalhire \u2014 sync your profile (Tier-1, opt-in) \u2502");
2809
+ console.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");
2810
+ console.log("");
2811
+ console.log(" You are about to send the following data to");
2812
+ console.log(" staqs (terminalhire.com):");
2813
+ console.log("");
2814
+ for (const f of fields) {
2815
+ const shown = Array.isArray(f.value) ? JSON.stringify(f.value) : String(f.value ?? "(not set)");
2816
+ console.log(` ${f.label.padEnd(16)}: ${shown}`);
2817
+ }
2818
+ console.log("");
2819
+ console.log(" What is NEVER sent:");
2820
+ console.log(" - Private repos, employer repos, raw code");
2821
+ console.log(" - Employer-repo-derived tags (filtered at source)");
2822
+ console.log(" - Session history, file paths, project names");
2823
+ console.log("");
2824
+ console.log(" This is a ONE-TIME snapshot. Nothing is sent automatically;");
2825
+ console.log(" re-run `terminalhire sync --push` to update it later.");
2826
+ console.log(" Delete it any time: terminalhire sync --delete");
2827
+ console.log("");
2828
+ console.log(" This is NOT required to use terminalhire.");
2829
+ console.log("");
2830
+ }
2831
+ async function runPush() {
2832
+ const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
2833
+ const profile = await readProfile2();
2834
+ if (!profile.github || !profile.github.login) {
2835
+ console.log("");
2836
+ console.log(" No GitHub data in your local profile yet.");
2837
+ console.log(" Run `terminalhire login` first, then re-run `terminalhire sync --push`.");
2838
+ console.log("");
2839
+ process.exit(1);
2840
+ }
2841
+ const fields = buildConsentFields(profile);
2842
+ renderConsentCard(fields);
2843
+ let consentConfirmed = false;
2844
+ const answer = await ask2(' Sync your profile to staqs (terminalhire.com)? Type "yes" to continue: ');
2845
+ if (answer === "yes") {
2846
+ consentConfirmed = true;
2847
+ }
2848
+ if (!consentConfirmed) {
2849
+ console.log("\n Aborted \u2014 nothing was sent.\n");
2850
+ process.exit(0);
2851
+ }
2852
+ const consentedAt = (/* @__PURE__ */ new Date()).toISOString();
2853
+ const consentToken = {
2854
+ consentedAt,
2855
+ version: CONSENT_VERSION,
2856
+ fields: fields.map((f) => f.key)
2857
+ };
2858
+ const payloadProfile = {
2859
+ login: profile.github.login,
2860
+ name: profile.displayName || null,
2861
+ publicEmail: profile.github.publicEmail || null,
2862
+ topLanguages: profile.github.topLanguages || [],
2863
+ skillTags: profile.skillTags || [],
2864
+ displayName: profile.displayName || null,
2865
+ contactEmail: profile.contactEmail || null
2866
+ };
2867
+ const priorMarker = readMarker();
2868
+ const rowToken = priorMarker && priorMarker.deleteToken ? priorMarker.deleteToken : null;
2869
+ const requestBody = { consentToken, profile: payloadProfile };
2870
+ if (rowToken) {
2871
+ requestBody.rowToken = rowToken;
2872
+ }
2873
+ console.log("\n Sending one-time snapshot...");
2874
+ let res;
2875
+ try {
2876
+ res = await fetch(`${API_URL2}/api/profile-sync`, {
2877
+ method: "POST",
2878
+ headers: { "Content-Type": "application/json" },
2879
+ body: JSON.stringify(requestBody),
2880
+ signal: AbortSignal.timeout(1e4)
2881
+ });
2882
+ } catch (err) {
2883
+ console.error(`
2884
+ Sync failed: ${err instanceof Error ? err.message : String(err)}`);
2885
+ process.exit(1);
2886
+ }
2887
+ if (!res.ok) {
2888
+ let detail = "";
2889
+ try {
2890
+ detail = (await res.json())?.message || "";
2891
+ } catch {
2892
+ }
2893
+ console.error(`
2894
+ Sync failed: /api/profile-sync returned ${res.status}. ${detail}`);
2895
+ if (res.status === 503) {
2896
+ console.error(" (Tier-1 sync is not enabled on the server yet.)");
2897
+ }
2898
+ if (res.status === 403) {
2899
+ console.error(" (This GitHub login was already claimed by a different push.)");
2900
+ }
2901
+ process.exit(1);
2902
+ }
2903
+ let deleteToken = null;
2904
+ try {
2905
+ deleteToken = (await res.json())?.deleteToken || null;
2906
+ } catch {
2907
+ }
2908
+ writeMarker({ consentedAt, login: profile.github.login, deleteToken });
2909
+ console.log("\n Profile synced. A local consent marker was written to ~/.terminalhire/tier1.json");
2910
+ console.log(" Delete your synced profile any time: terminalhire sync --delete\n");
2911
+ }
2912
+ function runStatus() {
2913
+ const marker = readMarker();
2914
+ console.log("");
2915
+ if (marker && marker.consentedAt) {
2916
+ console.log(" Tier-1 sync: CONSENTED (local marker present)");
2917
+ console.log(` login: ${marker.login || "(unknown)"}`);
2918
+ console.log(` consented at: ${marker.consentedAt}`);
2919
+ console.log("");
2920
+ console.log(" This reflects your local marker only (no network call was made).");
2921
+ console.log(" Update: terminalhire sync --push | Delete: terminalhire sync --delete");
2922
+ } else {
2923
+ console.log(" Tier-1 sync: NOT CONSENTED (no local marker).");
2924
+ console.log(" Your profile has not been synced. Enable: terminalhire sync --push");
2925
+ }
2926
+ console.log("");
2927
+ }
2928
+ async function runDelete() {
2929
+ const marker = readMarker();
2930
+ console.log("");
2931
+ console.log(" This will hard-delete your synced profile from staqs (terminalhire.com)");
2932
+ console.log(" and remove the local consent marker. There is no soft-delete.");
2933
+ console.log("");
2934
+ const answer = await ask2(' Delete your synced profile? Type "yes" to confirm: ');
2935
+ if (answer !== "yes") {
2936
+ console.log("\n Aborted \u2014 nothing was deleted.\n");
2937
+ process.exit(0);
2938
+ }
2939
+ let login = marker && marker.login ? marker.login : null;
2940
+ if (!login) {
2941
+ const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
2942
+ const profile = await readProfile2();
2943
+ login = profile.github && profile.github.login ? profile.github.login : null;
2944
+ }
2945
+ if (!login) {
2946
+ console.log("\n No github login to delete (no marker, no GitHub profile). Clearing local marker.\n");
2947
+ clearMarker();
2948
+ process.exit(0);
2949
+ }
2950
+ const deleteToken = marker && marker.deleteToken ? marker.deleteToken : null;
2951
+ if (!deleteToken) {
2952
+ console.log("\n No delete token found in ~/.terminalhire/tier1.json.");
2953
+ console.log(" Deletion must be run from the machine that originally pushed your profile");
2954
+ console.log(" (the delete token is stored there), or re-run `terminalhire sync --push`");
2955
+ console.log(" first to obtain a fresh token, then `terminalhire sync --delete`.\n");
2956
+ process.exit(1);
2957
+ }
2958
+ const consentToken = {
2959
+ consentedAt: marker && marker.consentedAt || (/* @__PURE__ */ new Date()).toISOString(),
2960
+ version: CONSENT_VERSION,
2961
+ fields: ["login"]
2962
+ };
2963
+ console.log("\n Requesting deletion...");
2964
+ let res;
2965
+ try {
2966
+ res = await fetch(`${API_URL2}/api/profile-sync`, {
2967
+ method: "DELETE",
2968
+ headers: { "Content-Type": "application/json" },
2969
+ body: JSON.stringify({ consentToken, login, deleteToken }),
2970
+ signal: AbortSignal.timeout(1e4)
2971
+ });
2972
+ } catch (err) {
2973
+ console.error(`
2974
+ Delete failed: ${err instanceof Error ? err.message : String(err)}`);
2975
+ console.error(" Local marker NOT cleared (server state unknown). Re-run to retry.\n");
2976
+ process.exit(1);
2977
+ }
2978
+ if (!res.ok) {
2979
+ console.error(`
2980
+ Delete failed: /api/profile-sync returned ${res.status}.`);
2981
+ if (res.status === 503) {
2982
+ console.error(" (Tier-1 sync is not enabled on the server yet.) Clearing local marker.");
2983
+ clearMarker();
2984
+ }
2985
+ process.exit(1);
2986
+ }
2987
+ clearMarker();
2988
+ console.log("\n Synced profile deleted and local marker cleared.\n");
2989
+ }
2990
+ async function run7() {
2991
+ const args2 = process.argv.slice(2).filter((a) => a !== "sync");
2992
+ const has = (f) => args2.includes(f);
2993
+ if (has("--push") || has("--enable")) {
2994
+ await runPush();
2995
+ return;
2996
+ }
2997
+ if (has("--status")) {
2998
+ runStatus();
2999
+ return;
3000
+ }
3001
+ if (has("--delete")) {
3002
+ await runDelete();
3003
+ return;
3004
+ }
3005
+ console.log("");
3006
+ console.log(" terminalhire sync \u2014 opt-in Tier-1 profile sync (one-time snapshot)");
3007
+ console.log("");
3008
+ console.log(' terminalhire sync --push Send your profile (shows a consent card, requires typed "yes")');
3009
+ console.log(" terminalhire sync --status Show whether you have consented (local read only)");
3010
+ console.log(" terminalhire sync --delete Hard-delete your synced profile (revocation)");
3011
+ console.log("");
3012
+ console.log(' Your profile is NEVER sent without an explicit typed "yes".');
3013
+ console.log(" This is NOT required to use terminalhire.");
3014
+ console.log("");
3015
+ }
3016
+ var TH_DIR3, TIER1_MARKER, API_URL2, CONSENT_VERSION;
3017
+ var init_jpi_sync = __esm({
3018
+ "bin/jpi-sync.js"() {
3019
+ "use strict";
3020
+ TH_DIR3 = process.env["TERMINALHIRE_DIR"] || join9(homedir7(), ".terminalhire");
3021
+ TIER1_MARKER = join9(TH_DIR3, "tier1.json");
3022
+ API_URL2 = process.env["TERMINALHIRE_API_URL"] || process.env["JPI_API_URL"] || "https://terminalhire.com";
3023
+ CONSENT_VERSION = 1;
3024
+ }
3025
+ });
3026
+
2746
3027
  // bin/jpi-init.js
2747
3028
  var jpi_init_exports = {};
2748
3029
  __export(jpi_init_exports, {
2749
- run: () => run7
3030
+ run: () => run8
2750
3031
  });
2751
- import { existsSync as existsSync7 } from "fs";
2752
- import { join as join9, resolve } from "path";
3032
+ import { existsSync as existsSync8 } from "fs";
3033
+ import { join as join10, resolve } from "path";
2753
3034
  import { fileURLToPath as fileURLToPath3 } from "url";
2754
- import { createInterface as createInterface4 } from "readline";
3035
+ import { createInterface as createInterface5 } from "readline";
2755
3036
  import { spawnSync, spawn } from "child_process";
2756
- import { homedir as homedir7 } from "os";
2757
- function ask2(question) {
2758
- const rl = createInterface4({ input: process.stdin, output: process.stdout });
3037
+ import { homedir as homedir8 } from "os";
3038
+ function ask3(question) {
3039
+ const rl = createInterface5({ input: process.stdin, output: process.stdout });
2759
3040
  return new Promise((resolve2) => {
2760
3041
  rl.question(question, (answer) => {
2761
3042
  rl.close();
@@ -2764,18 +3045,18 @@ function ask2(question) {
2764
3045
  });
2765
3046
  }
2766
3047
  function resolveScript(name) {
2767
- const distPath = resolve(join9(__dirname2, "..", "..", "dist", "bin", `${name}.js`));
2768
- const legacyPath = resolve(join9(__dirname2, `${name}.js`));
2769
- return existsSync7(distPath) ? distPath : legacyPath;
3048
+ const distPath = resolve(join10(__dirname2, "..", "..", "dist", "bin", `${name}.js`));
3049
+ const legacyPath = resolve(join10(__dirname2, `${name}.js`));
3050
+ return existsSync8(distPath) ? distPath : legacyPath;
2770
3051
  }
2771
3052
  function resolveInstallJs() {
2772
- const fromDist = resolve(join9(__dirname2, "..", "..", "install.js"));
2773
- const fromBin = resolve(join9(__dirname2, "..", "install.js"));
2774
- if (existsSync7(fromDist)) return fromDist;
2775
- if (existsSync7(fromBin)) return fromBin;
3053
+ const fromDist = resolve(join10(__dirname2, "..", "..", "install.js"));
3054
+ const fromBin = resolve(join10(__dirname2, "..", "install.js"));
3055
+ if (existsSync8(fromDist)) return fromDist;
3056
+ if (existsSync8(fromBin)) return fromBin;
2776
3057
  return fromBin;
2777
3058
  }
2778
- async function run7() {
3059
+ async function run8() {
2779
3060
  console.log("");
2780
3061
  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");
2781
3062
  console.log("\u2502 terminalhire init \u2014 one-command onboarding \u2502");
@@ -2785,7 +3066,7 @@ async function run7() {
2785
3066
  console.log("This will:");
2786
3067
  console.log(" 1. Optionally sign you in with GitHub (public profile only, read:user)");
2787
3068
  console.log(" 2. Seed your local job cache (anonymous index download)");
2788
- console.log(" 3. Install the statusLine hook in ~/.claude/settings.json");
3069
+ console.log(" 3. Enable the ambient spinner job surface in ~/.claude/settings.json");
2789
3070
  console.log(" (with backup + your explicit consent before any file is touched)");
2790
3071
  console.log("");
2791
3072
  console.log('You can stop at any step. Nothing is changed until you say "yes".');
@@ -2797,7 +3078,7 @@ async function run7() {
2797
3078
  console.log(" GitHub data enriches your local profile. Nothing leaves your machine");
2798
3079
  console.log(" until you explicitly consent to a specific lead.");
2799
3080
  console.log("");
2800
- const githubAnswer = await ask2("Sign in with GitHub now? [Y/n] (Enter = yes, n = stay local): ");
3081
+ const githubAnswer = await ask3("Sign in with GitHub now? [Y/n] (Enter = yes, n = stay local): ");
2801
3082
  const doGitHub = githubAnswer === "" || githubAnswer === "y" || githubAnswer === "yes";
2802
3083
  if (doGitHub) {
2803
3084
  console.log("");
@@ -2838,11 +3119,11 @@ async function run7() {
2838
3119
  console.log(" Run `terminalhire jobs` after a few Claude Code sessions to populate it.");
2839
3120
  }
2840
3121
  console.log("");
2841
- console.log("Step 3/3 \u2014 Install statusLine hook in ~/.claude/settings.json");
3122
+ console.log("Step 3/3 \u2014 Enable the ambient spinner job surface in ~/.claude/settings.json");
2842
3123
  console.log("");
2843
3124
  console.log(" This is the only step that modifies a system file.");
2844
3125
  console.log(" A timestamped backup is created before any change.");
2845
- console.log(" Uninstall at any time: node install.js --uninstall");
3126
+ console.log(" Disable at any time: node install.js --uninstall (or terminalhire spinner --off)");
2846
3127
  console.log("");
2847
3128
  const installJs = resolveInstallJs();
2848
3129
  const installChild = spawnSync(process.execPath, [installJs], {
@@ -2857,11 +3138,11 @@ async function run7() {
2857
3138
  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");
2858
3139
  console.log("\u2502 terminalhire init complete! \u2502");
2859
3140
  console.log("\u2502 \u2502");
2860
- console.log("\u2502 Restart Claude Code to see the statusLine nudge. \u2502");
3141
+ console.log("\u2502 Restart Claude Code to see the ambient spinner job surface. \u2502");
2861
3142
  console.log("\u2502 \u2502");
2862
3143
  console.log("\u2502 Quick reference: \u2502");
2863
3144
  console.log("\u2502 terminalhire jobs \u2014 browse matching roles \u2502");
2864
- console.log("\u2502 terminalhire config --show \u2014 view nudge settings \u2502");
3145
+ console.log("\u2502 terminalhire spinner --off \u2014 disable the spinner surface \u2502");
2865
3146
  console.log("\u2502 terminalhire login \u2014 sign in with GitHub \u2502");
2866
3147
  console.log("\u2502 terminalhire profile --show \u2014 inspect your local profile \u2502");
2867
3148
  console.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\u2500\u2500\u2500\u2500\u2518");
@@ -2878,17 +3159,17 @@ var init_jpi_init = __esm({
2878
3159
  // bin/jpi-refresh.js
2879
3160
  var jpi_refresh_exports = {};
2880
3161
  __export(jpi_refresh_exports, {
2881
- run: () => run8
3162
+ run: () => run9
2882
3163
  });
2883
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync8, mkdirSync as mkdirSync7 } from "fs";
2884
- import { join as join10 } from "path";
2885
- import { homedir as homedir8 } from "os";
3164
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, existsSync as existsSync9, mkdirSync as mkdirSync8 } from "fs";
3165
+ import { join as join11 } from "path";
3166
+ import { homedir as homedir9 } from "os";
2886
3167
  import { fileURLToPath as fileURLToPath4 } from "url";
2887
- async function run8() {
3168
+ async function run9() {
2888
3169
  try {
2889
3170
  let index;
2890
3171
  try {
2891
- const res = await fetch(`${API_URL2}/api/index`, {
3172
+ const res = await fetch(`${API_URL3}/api/index`, {
2892
3173
  signal: AbortSignal.timeout(15e3),
2893
3174
  headers: { "Accept": "application/json" }
2894
3175
  });
@@ -2915,7 +3196,7 @@ async function run8() {
2915
3196
  const fp = profileToFingerprint2(profile);
2916
3197
  const results = match2(fp, jobs, jobs.length);
2917
3198
  matchCount = results.length;
2918
- topMatches = results.slice(0, 8).map((r) => ({
3199
+ topMatches = results.slice(0, 25).map((r) => ({
2919
3200
  id: r.job.id,
2920
3201
  title: r.job.title,
2921
3202
  company: r.job.company,
@@ -2926,14 +3207,14 @@ async function run8() {
2926
3207
  }
2927
3208
  } catch {
2928
3209
  }
2929
- mkdirSync7(TERMINALHIRE_DIR5, { recursive: true });
3210
+ mkdirSync8(TERMINALHIRE_DIR5, { recursive: true });
2930
3211
  const cacheEntry = {
2931
3212
  ts: Date.now(),
2932
3213
  index,
2933
3214
  matchCount,
2934
3215
  topMatches
2935
3216
  };
2936
- writeFileSync7(INDEX_CACHE_FILE2, JSON.stringify(cacheEntry), "utf8");
3217
+ writeFileSync8(INDEX_CACHE_FILE2, JSON.stringify(cacheEntry), "utf8");
2937
3218
  try {
2938
3219
  const {
2939
3220
  readSpinnerConfig: readSpinnerConfig2,
@@ -2960,7 +3241,7 @@ async function run8() {
2960
3241
  const verbs = buildSpinnerPool2(ranked, sc.max, { sessionTags, frequency: sc.frequency });
2961
3242
  if (verbs.length > 0) applySpinnerVerbs2(verbs, sc.mode);
2962
3243
  else clearSpinnerVerbs2();
2963
- const tips = buildTips2(ranked, API_URL2, 3);
3244
+ const tips = buildTips2(ranked, API_URL3, 8);
2964
3245
  if (tips.length > 0) applySpinnerTips2(tips);
2965
3246
  else clearSpinnerTips2();
2966
3247
  } else {
@@ -2977,30 +3258,30 @@ async function run8() {
2977
3258
  process.exit(1);
2978
3259
  }
2979
3260
  }
2980
- var __dirname3, TERMINALHIRE_DIR5, INDEX_CACHE_FILE2, API_URL2;
3261
+ var __dirname3, TERMINALHIRE_DIR5, INDEX_CACHE_FILE2, API_URL3;
2981
3262
  var init_jpi_refresh = __esm({
2982
3263
  "bin/jpi-refresh.js"() {
2983
3264
  "use strict";
2984
3265
  __dirname3 = fileURLToPath4(new URL(".", import.meta.url));
2985
- TERMINALHIRE_DIR5 = join10(homedir8(), ".terminalhire");
2986
- INDEX_CACHE_FILE2 = join10(TERMINALHIRE_DIR5, "index-cache.json");
2987
- API_URL2 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
3266
+ TERMINALHIRE_DIR5 = join11(homedir9(), ".terminalhire");
3267
+ INDEX_CACHE_FILE2 = join11(TERMINALHIRE_DIR5, "index-cache.json");
3268
+ API_URL3 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
2988
3269
  }
2989
3270
  });
2990
3271
 
2991
3272
  // bin/jpi-save.js
2992
3273
  var jpi_save_exports = {};
2993
3274
  __export(jpi_save_exports, {
2994
- run: () => run9
3275
+ run: () => run10
2995
3276
  });
2996
- import { readFileSync as readFileSync10, existsSync as existsSync9 } from "fs";
2997
- import { join as join11 } from "path";
2998
- import { homedir as homedir9 } from "os";
3277
+ import { readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
3278
+ import { join as join12 } from "path";
3279
+ import { homedir as homedir10 } from "os";
2999
3280
  import { fileURLToPath as fileURLToPath5 } from "url";
3000
3281
  function findJobInCache(jobId) {
3001
3282
  try {
3002
- if (!existsSync9(INDEX_CACHE_FILE3)) return null;
3003
- const raw = readFileSync10(INDEX_CACHE_FILE3, "utf8");
3283
+ if (!existsSync10(INDEX_CACHE_FILE3)) return null;
3284
+ const raw = readFileSync11(INDEX_CACHE_FILE3, "utf8");
3004
3285
  const entry = JSON.parse(raw);
3005
3286
  const jobs = entry?.index?.jobs ?? [];
3006
3287
  return jobs.find((j) => j.id === jobId) ?? null;
@@ -3069,7 +3350,7 @@ async function cmdUnsave(jobId) {
3069
3350
  process.exit(1);
3070
3351
  }
3071
3352
  }
3072
- async function run9() {
3353
+ async function run10() {
3073
3354
  const verb = process.argv[2];
3074
3355
  const jobId = process.argv[3];
3075
3356
  try {
@@ -3093,26 +3374,26 @@ var init_jpi_save = __esm({
3093
3374
  "bin/jpi-save.js"() {
3094
3375
  "use strict";
3095
3376
  __dirname4 = fileURLToPath5(new URL(".", import.meta.url));
3096
- TERMINALHIRE_DIR6 = join11(homedir9(), ".terminalhire");
3097
- INDEX_CACHE_FILE3 = join11(TERMINALHIRE_DIR6, "index-cache.json");
3377
+ TERMINALHIRE_DIR6 = join12(homedir10(), ".terminalhire");
3378
+ INDEX_CACHE_FILE3 = join12(TERMINALHIRE_DIR6, "index-cache.json");
3098
3379
  }
3099
3380
  });
3100
3381
 
3101
3382
  // bin/jpi-dispatch.js
3102
3383
  import { fileURLToPath as fileURLToPath6 } from "url";
3103
- import { join as join12, dirname as dirname2 } from "path";
3104
- import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
3384
+ import { join as join13, dirname as dirname2 } from "path";
3385
+ import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
3105
3386
  import { createRequire } from "module";
3106
3387
  var __dirname5 = fileURLToPath6(new URL(".", import.meta.url));
3107
3388
  function readPackageVersion() {
3108
3389
  try {
3109
3390
  const candidates = [
3110
- join12(__dirname5, "..", "..", "package.json"),
3111
- join12(__dirname5, "..", "package.json")
3391
+ join13(__dirname5, "..", "..", "package.json"),
3392
+ join13(__dirname5, "..", "package.json")
3112
3393
  ];
3113
3394
  for (const p of candidates) {
3114
- if (existsSync10(p)) {
3115
- const pkg = JSON.parse(readFileSync11(p, "utf8"));
3395
+ if (existsSync11(p)) {
3396
+ const pkg = JSON.parse(readFileSync12(p, "utf8"));
3116
3397
  if (pkg.version) return pkg.version;
3117
3398
  }
3118
3399
  }
@@ -3123,7 +3404,7 @@ function readPackageVersion() {
3123
3404
  var firstArg = process.argv[2];
3124
3405
  if (!firstArg && !process.stdin.isTTY) {
3125
3406
  const { default: childProcess } = await import("child_process");
3126
- const nudgeScript = join12(__dirname5, "jpi.js");
3407
+ const nudgeScript = join13(__dirname5, "jpi.js");
3127
3408
  const child = childProcess.spawnSync(process.execPath, [nudgeScript], {
3128
3409
  stdio: ["inherit", "inherit", "inherit"]
3129
3410
  });
@@ -3154,6 +3435,9 @@ if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-
3154
3435
  console.log(" terminalhire save <jobId> Save a job locally (id shown in `jobs` output)");
3155
3436
  console.log(" terminalhire saved List all locally-saved jobs");
3156
3437
  console.log(" terminalhire unsave <jobId> Remove a saved job");
3438
+ console.log(" terminalhire sync --push Opt-in: send your profile to staqs (typed-yes consent)");
3439
+ console.log(" terminalhire sync --status Show whether you have consented (local read only)");
3440
+ console.log(" terminalhire sync --delete Hard-delete your synced profile (revocation)");
3157
3441
  console.log("");
3158
3442
  console.log("Install / uninstall the Claude Code statusLine nudge:");
3159
3443
  console.log(" terminalhire init (preferred \u2014 full guided onboarding)");
@@ -3163,6 +3447,7 @@ if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-
3163
3447
  console.log("Privacy: your profile never leaves your device.");
3164
3448
  console.log(" GET /api/index \u2014 anonymous index download (no dev data)");
3165
3449
  console.log(" POST /api/lead \u2014 only after explicit per-role named-entity consent");
3450
+ console.log(" POST /api/profile-sync \u2014 only via `terminalhire sync --push` with a typed-yes consent token");
3166
3451
  console.log(" GitHub token \u2014 encrypted at ~/.terminalhire/github-token.enc, scope: read:user");
3167
3452
  console.log("");
3168
3453
  process.exit(0);
@@ -3202,6 +3487,11 @@ if (firstArg === "spinner") {
3202
3487
  await mod.run();
3203
3488
  process.exit(0);
3204
3489
  }
3490
+ if (firstArg === "sync") {
3491
+ const mod = await Promise.resolve().then(() => (init_jpi_sync(), jpi_sync_exports));
3492
+ await mod.run();
3493
+ process.exit(0);
3494
+ }
3205
3495
  if (firstArg === "init") {
3206
3496
  const mod = await Promise.resolve().then(() => (init_jpi_init(), jpi_init_exports));
3207
3497
  await mod.run();
@@ -39,7 +39,7 @@ async function run() {
39
39
  console.log("This will:");
40
40
  console.log(" 1. Optionally sign you in with GitHub (public profile only, read:user)");
41
41
  console.log(" 2. Seed your local job cache (anonymous index download)");
42
- console.log(" 3. Install the statusLine hook in ~/.claude/settings.json");
42
+ console.log(" 3. Enable the ambient spinner job surface in ~/.claude/settings.json");
43
43
  console.log(" (with backup + your explicit consent before any file is touched)");
44
44
  console.log("");
45
45
  console.log('You can stop at any step. Nothing is changed until you say "yes".');
@@ -92,11 +92,11 @@ async function run() {
92
92
  console.log(" Run `terminalhire jobs` after a few Claude Code sessions to populate it.");
93
93
  }
94
94
  console.log("");
95
- console.log("Step 3/3 \u2014 Install statusLine hook in ~/.claude/settings.json");
95
+ console.log("Step 3/3 \u2014 Enable the ambient spinner job surface in ~/.claude/settings.json");
96
96
  console.log("");
97
97
  console.log(" This is the only step that modifies a system file.");
98
98
  console.log(" A timestamped backup is created before any change.");
99
- console.log(" Uninstall at any time: node install.js --uninstall");
99
+ console.log(" Disable at any time: node install.js --uninstall (or terminalhire spinner --off)");
100
100
  console.log("");
101
101
  const installJs = resolveInstallJs();
102
102
  const installChild = spawnSync(process.execPath, [installJs], {
@@ -111,11 +111,11 @@ async function run() {
111
111
  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");
112
112
  console.log("\u2502 terminalhire init complete! \u2502");
113
113
  console.log("\u2502 \u2502");
114
- console.log("\u2502 Restart Claude Code to see the statusLine nudge. \u2502");
114
+ console.log("\u2502 Restart Claude Code to see the ambient spinner job surface. \u2502");
115
115
  console.log("\u2502 \u2502");
116
116
  console.log("\u2502 Quick reference: \u2502");
117
117
  console.log("\u2502 terminalhire jobs \u2014 browse matching roles \u2502");
118
- console.log("\u2502 terminalhire config --show \u2014 view nudge settings \u2502");
118
+ console.log("\u2502 terminalhire spinner --off \u2014 disable the spinner surface \u2502");
119
119
  console.log("\u2502 terminalhire login \u2014 sign in with GitHub \u2502");
120
120
  console.log("\u2502 terminalhire profile --show \u2014 inspect your local profile \u2502");
121
121
  console.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\u2500\u2500\u2500\u2500\u2518");